Merge pull request #49 from fabioformosa/feature/#14

Feature/#14
This commit is contained in:
Fabio Formosa
2021-11-01 16:20:23 +01:00
committed by GitHub
50 changed files with 1635 additions and 535 deletions

View File

@@ -108,11 +108,11 @@
"defaultProject": "angular-spring-starter",
"schematics": {
"@schematics/angular:component": {
"prefix": "app",
"prefix": "qrzmng",
"style": "css"
},
"@schematics/angular:directive": {
"prefix": "app"
"prefix": "qrzmng"
}
}
}
}

View File

@@ -2,35 +2,64 @@
<mat-card-header>
<mat-card-title><b>SCHEDULER CONFIG</b></mat-card-title>
</mat-card-header>
<mat-card-content>
<!-- ADD BUTTON -->
<mat-card-content *ngIf="!existsATriggerInProgress() && !enabledTriggerForm">
<button mat-fab color="primary">
<mat-icon (click)="enableTriggerForm()">add</mat-icon>
</button>
</mat-card-content>
<!-- TRIGGER DETAILS -->
<mat-card-content *ngIf="existsATriggerInProgress() || enabledTriggerForm">
<div fxLayout="column">
<form name="configForm" fxFlex="1 1 100%" #configForm="ngForm">
<mat-form-field>
<input matInput placeholder="Freq [Num per day]" [(ngModel)]="config.triggerPerDay" name="triggerPerDay" type="number">
<mat-form-field [appearance]="enabledTriggerForm ? 'fill': 'none'">
<mat-label>Freq [Num per day]</mat-label>
<input [readonly]="!enabledTriggerForm"
matInput placeholder="Freq [Num per day]" name="triggerPerDay" type="number"
[(ngModel)]="config.triggerPerDay"
>
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Max Occurrences" [(ngModel)]="config.maxCount" name="maxCount" type="number">
<mat-form-field [appearance]="enabledTriggerForm ? 'fill': 'none'">
<mat-label>Max Occurrences</mat-label>
<input [readonly]="!enabledTriggerForm"
matInput placeholder="Max Occurrences" name="maxCount" type="number"
[(ngModel)]="config.maxCount"
>
</mat-form-field>
<br>
<div>
<h5>Misfire Policy</h5>
<div>RESCHEDULE NEXT WITH REMAINING COUNT</div>
<div class="small">
In case of misfire event, the trigger is re-scheduled to the next scheduled time after 'now' with the repeat count set to what it would be, if it had not missed any firings.
In case of misfire event, the trigger is re-scheduled to the next scheduled time after 'now' with the repeat count set to what it would be, if it had not missed any firings.
<br/>
<strong>Warning:</strong> This policy could cause the Trigger to go directly to the 'COMPLETE' state if all fire-times where missed.
<strong>Warning:</strong> This policy could cause the Trigger to go directly to the 'COMPLETE' state if all fire-times where missed.
</div>
</div>
<br>
<button mat-raised-button
type="button"
*ngIf="enabledTriggerForm"
(click)="cancelConfigForm()">
Cancel
</button>
<button mat-raised-button
type="button"
type="button" color="primary"
*ngIf="enabledTriggerForm"
(click)="submitConfig()">
Submit
</button>
<button mat-raised-button type="button"
*ngIf="!enabledTriggerForm"
(click)="enabledTriggerForm = true">
Reschedule
</button>
</form>
</div>
</mat-card-content>

View File

@@ -1,40 +1,69 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { SchedulerService } from '../../services';
import { SchedulerConfig } from '../../model/schedulerConfig.model'
import {Scheduler} from '../../model/scheduler.model';
@Component({
selector: 'scheduler-config',
selector: 'qrzmng-scheduler-config',
templateUrl: './scheduler-config.component.html',
styleUrls: ['./scheduler-config.component.scss']
})
export class SchedulerConfigComponent implements OnInit {
config: SchedulerConfig = new SchedulerConfig()
configBackup: SchedulerConfig = new SchedulerConfig()
scheduler: Scheduler;
triggerLoading = true;
enabledTriggerForm = false;
private fetchedTriggers = false;
private triggerInProgress = false;
constructor(
private schedulerService: SchedulerService
) { }
config : SchedulerConfig = new SchedulerConfig()
configBackup : SchedulerConfig = new SchedulerConfig()
ngOnInit() {
this.retrieveConfig()
this.triggerLoading = true;
this._getScheduler();
this.retrieveConfig();
}
retrieveConfig = () => {
this.schedulerService.getConfig()
.subscribe(res => {
this.config = new SchedulerConfig(res.triggerPerDay, res.maxCount)
this.config = new SchedulerConfig(res.triggerPerDay, res.maxCount, res.timesTriggered)
this.configBackup = res
this.triggerLoading = false;
this.triggerInProgress = res.timesTriggered < res.maxCount;
})
}
private _getScheduler() {
this.schedulerService.getScheduler()
.subscribe( res => {
this.scheduler = <Scheduler>res;
this.fetchedTriggers = this.scheduler.triggerKeys.length > 0
})
}
existsATriggerInProgress = (): boolean => this.fetchedTriggers && this.triggerInProgress;
cancelConfigForm = () => this.enabledTriggerForm = false;
submitConfig = () => {
this.schedulerService.updateConfig(this.config)
const schedulerServiceCall = this.existsATriggerInProgress() ? this.schedulerService.updateConfig : this.schedulerService.saveConfig;
schedulerServiceCall(this.config)
.subscribe(res => {
this.configBackup = this.config;
this.enabledTriggerForm = false;
this.fetchedTriggers = true;
this.triggerInProgress = true;
}, error => {
this.config = this.configBackup;
});
};
enableTriggerForm = () => this.enabledTriggerForm = true;
}

View File

@@ -0,0 +1,13 @@
import {TriggerKey} from './triggerKey.model';
export class Scheduler {
name: string;
instanceId: string;
triggerKeys: TriggerKey[];
constructor(name: string, instanceId: string, triggerKeys: TriggerKey[]) {
this.name = name;
this.instanceId = instanceId;
this.triggerKeys = triggerKeys;
}
}

View File

@@ -1,11 +1,13 @@
export class SchedulerConfig {
triggerPerDay : number = 0
maxCount : number = 0
triggerPerDay = 0;
maxCount = 0;
timesTriggered = 0;
constructor(triggerPerDay = 0, maxCount = 0) {
this.triggerPerDay = triggerPerDay
this.maxCount = maxCount
constructor(triggerPerDay = 0, maxCount = 0, timesTriggered = 0) {
this.triggerPerDay = triggerPerDay;
this.maxCount = maxCount;
this.timesTriggered = timesTriggered;
}
}
}

View File

@@ -0,0 +1,9 @@
export class TriggerKey {
name: string;
group: string;
constructor(name: string, group: string) {
this.name = name;
this.group = group;
}
}

View File

@@ -12,20 +12,24 @@ export class SchedulerService {
startScheduler = () => {
return this.apiService.get(getBaseUrl() + '/quartz-manager/scheduler/run')
}
stopScheduler = () => {
return this.apiService.get(getBaseUrl() + '/quartz-manager/scheduler/stop')
}
pauseScheduler = () => {
return this.apiService.get(getBaseUrl() + '/quartz-manager/scheduler/pause')
}
resumeScheduler = () => {
return this.apiService.get(getBaseUrl() + '/quartz-manager/scheduler/resume')
}
getStatus = () => {
return this.apiService.get(getBaseUrl() + '/quartz-manager/scheduler/status')
}
getScheduler = () => {
return this.apiService.get(getBaseUrl() + '/quartz-manager/scheduler')
}
@@ -33,7 +37,13 @@ export class SchedulerService {
return this.apiService.get(getBaseUrl() + '/quartz-manager/scheduler/config')
}
updateConfig = (config: Object) => {
return this.apiService.post(getBaseUrl() + '/quartz-manager/scheduler/config', config)
saveConfig = (config: Object) => {
return this.apiService.post(getBaseUrl() + '/quartz-manager/triggers/mytrigger', config)
}
updateConfig = (config: Object) => {
return this.apiService.put(getBaseUrl() + '/quartz-manager/triggers/mytrigger', config)
}
}

View File

@@ -4,16 +4,16 @@
<div fxLayout="column">
<scheduler-control></scheduler-control>
<br/>
<scheduler-config></scheduler-config>
<qrzmng-scheduler-config></qrzmng-scheduler-config>
</div>
</div>
</div>
<div fxFlex="1 1 70%" style="margin-left: 20px">
<div fxLayout="column">
<div fxFlex="1 1 100%"><progress-panel></progress-panel></div>
<br/>
<div fxFlex="1 1 100%"><logs-panel></logs-panel></div>
</div>
</div>
</div>
</div>

View File

@@ -3,3 +3,4 @@
/**/target
.classpath
.project
.idea

View File

@@ -44,10 +44,17 @@
<module>quartz-manager-starter-ui</module>
<module>quartz-manager-starter-security</module>
<module>quartz-manager-web-showcase</module>
<module>quartz-manager-starter-persistence</module>
<module>quartz-manager-common</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-common</artifactId>
<version>3.0.2-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-api</artifactId>
@@ -58,6 +65,11 @@
<artifactId>quartz-manager-starter-security</artifactId>
<version>3.0.2-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-persistence</artifactId>
<version>3.0.2-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-ui</artifactId>

View File

@@ -0,0 +1,23 @@
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>3.0.2-SNAPSHOT</version>
</parent>
<artifactId>quartz-manager-common</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,12 @@
package it.fabioformosa.quartzmanager.common.properties;
import lombok.Data;
import java.util.Properties;
@Data
public class QuartzModuleProperties{
private Properties properties = new Properties();
}

View File

@@ -0,0 +1,18 @@
package it.fabioformosa.quartzmanager.common.utils;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
public class DateUtils {
static public Date fromLocaleDateTimeToDate(LocalDateTime localDateTime){
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
}
static public Date getHoursFromNow(long hours){
return DateUtils.fromLocaleDateTimeToDate(LocalDateTime.now().plus(Duration.ofHours(hours)));
}
}

View File

@@ -0,0 +1,54 @@
package it.fabioformosa.quartzmanager.common.utils;
import java.util.function.Function;
public class Try<R> {
private final Throwable failure;
private final R success;
public Try(Throwable failure, R success) {
this.failure = failure;
this.success = success;
}
public R getSuccess() {
return success;
}
public static <R> Try success(R r){
return new Try<>(null, r);
}
public static <ExceptionType> Try failure(Throwable e){
return new Try<>(e, null);
}
public static <T, R> Function<T, Try<R>> with(CheckedFunction<T, R> checkedFunction){
return t -> {
try {
return Try.success(checkedFunction.apply(t));
} catch (java.lang.Exception e) {
return Try.failure(e);
}
};
}
public static <T, R> Function<T, R> sneakyThrow(CheckedFunction<T, R> checkedFunction){
return t -> Try.with(checkedFunction).apply(t).getSuccess();
}
public boolean isSuccess(){
return this.failure == null;
}
public boolean isFailure(){
return this.failure != null;
}
@FunctionalInterface
public static interface CheckedFunction<T, R> {
R apply(T t) throws java.lang.Exception;
}
}

View File

@@ -1,124 +1,142 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>3.0.2-SNAPSHOT</version>
</parent>
<artifactId>quartz-manager-starter-api</artifactId>
<name>Quartz Manager Starter API</name>
<description>REST API layer for your scheduler and triggered jobs, to be included in your spring webapp</description>
<url>https://github.com/fabioformosa/quartz-manager</url>
<properties>
<main.basedir>${basedir}/../..</main.basedir>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<springfox.version>2.9.2</springfox.version>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- SPRING -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- MISC -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- QUARTZ -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
<!-- Reactor -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-net</artifactId>
<version>2.0.8.RELEASE</version>
</dependency>
<dependency>
<groupId>io.projectreactor.spring</groupId>
<artifactId>reactor-spring-context</artifactId>
<version>2.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>
<!-- SWAGGER -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox.version}</version>
</dependency>
</dependencies>
</project>
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>3.0.2-SNAPSHOT</version>
</parent>
<artifactId>quartz-manager-starter-api</artifactId>
<name>Quartz Manager Starter API</name>
<description>REST API layer for your scheduler and triggered jobs, to be included in your spring webapp</description>
<url>https://github.com/fabioformosa/quartz-manager</url>
<properties>
<main.basedir>${basedir}/../..</main.basedir>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<springfox.version>2.9.2</springfox.version>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-common</artifactId>
</dependency>
<!-- SPRING -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- MISC -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>it.fabioformosa</groupId>
<artifactId>metamorphosis-core</artifactId>
<version>3.0.0</version>
</dependency>
<!-- QUARTZ -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
<!-- Reactor -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-net</artifactId>
<version>2.0.8.RELEASE</version>
</dependency>
<dependency>
<groupId>io.projectreactor.spring</groupId>
<artifactId>reactor-spring-context</artifactId>
<version>2.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>
<!-- SWAGGER -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox.version}</version>
</dependency>
<!-- TEST -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -1,9 +1,8 @@
package it.fabioformosa.quartzmanager.aspects;
import javax.annotation.Resource;
import it.fabioformosa.quartzmanager.dto.TriggerStatus;
import it.fabioformosa.quartzmanager.services.SchedulerService;
import org.quartz.DailyTimeIntervalTrigger;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
@@ -11,8 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Component;
import it.fabioformosa.quartzmanager.dto.TriggerStatus;
import it.fabioformosa.quartzmanager.scheduler.TriggerMonitor;
import javax.annotation.Resource;
/**
*
@@ -28,11 +26,14 @@ public class WebSocketProgressNotifier implements ProgressNotifier {
@Autowired
private SimpMessageSendingOperations messagingTemplate;
@Resource
private Scheduler scheduler;
// @Resource
// private Scheduler scheduler;
@Resource
private TriggerMonitor triggerMonitor;
private SchedulerService schedulerService;
// @Resource
// private TriggerMonitor triggerMonitor;
//@AfterReturning("execution(* logAndSend(..))")
// @Override
@@ -44,7 +45,7 @@ public class WebSocketProgressNotifier implements ProgressNotifier {
public void send() throws SchedulerException {
TriggerStatus currTriggerStatus = new TriggerStatus();
Trigger trigger = scheduler.getTrigger(triggerMonitor.getTrigger().getKey());
Trigger trigger = schedulerService.getOneSimpleTrigger().get();
currTriggerStatus.setFinalFireTime(trigger.getFinalFireTime());
currTriggerStatus.setNextFireTime(trigger.getNextFireTime());
currTriggerStatus.setPreviousFireTime(trigger.getPreviousFireTime());
@@ -62,7 +63,7 @@ public class WebSocketProgressNotifier implements ProgressNotifier {
repeatCount = dailyTrigger.getRepeatCount();
}
Trigger jobTrigger = triggerMonitor.getTrigger();
Trigger jobTrigger = schedulerService.getOneSimpleTrigger().get();
if (jobTrigger != null && jobTrigger.getJobKey() != null) {
currTriggerStatus.setJobKey(jobTrigger.getJobKey().getName());
currTriggerStatus.setJobClass(jobTrigger.getClass().getSimpleName());

View File

@@ -0,0 +1,9 @@
package it.fabioformosa.quartzmanager.configuration;
import it.fabioformosa.metamorphosis.core.EnableMetamorphosisConversions;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableMetamorphosisConversions(basePackages = { "it.fabioformosa.quartzmanager" })
public class ConversionConfig {
}

View File

@@ -1,14 +1,10 @@
package it.fabioformosa.quartzmanager.configuration;
import java.io.IOException;
import java.util.Properties;
import it.fabioformosa.quartzmanager.common.properties.QuartzModuleProperties;
import it.fabioformosa.quartzmanager.scheduler.AutowiringSpringBeanJobFactory;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.spi.JobFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -19,19 +15,15 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean;
import it.fabioformosa.quartzmanager.scheduler.AutowiringSpringBeanJobFactory;
import it.fabioformosa.quartzmanager.scheduler.TriggerMonitor;
import it.fabioformosa.quartzmanager.scheduler.TriggerMonitorImpl;
import java.io.IOException;
import java.util.Properties;
@ComponentScan(basePackages = {"it.fabioformosa.quartzmanager.controllers"})
@Configuration
@ConditionalOnProperty(name = "quartz.enabled")
public class SchedulerConfig {
private static final int DEFAULT_MISFIRE_INSTRUCTION = SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT;
private static JobDetailFactoryBean createJobDetail(Class<? extends Job> jobClass) {
JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
factoryBean.setJobClass(jobClass);
@@ -39,30 +31,13 @@ public class SchedulerConfig {
return factoryBean;
}
private static SimpleTriggerFactoryBean createTrigger(JobDetail jobDetail, long pollFrequencyMs,
int repeatCount) {
SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
factoryBean.setJobDetail(jobDetail);
factoryBean.setStartDelay(3000L);
factoryBean.setRepeatInterval(pollFrequencyMs);
factoryBean.setRepeatCount(repeatCount);
factoryBean
.setMisfireInstruction(DEFAULT_MISFIRE_INSTRUCTION);// in case of misfire, ignore all missed triggers and continue
return factoryBean;
}
@Value("${quartz-manager.jobClass}")
private String jobClassname;
@Bean(name = "triggerMonitor")
public TriggerMonitor createTriggerMonitor(@Qualifier("jobTrigger") Trigger trigger) {
TriggerMonitor triggerMonitor = new TriggerMonitorImpl();
triggerMonitor.setTrigger(trigger);
return triggerMonitor;
}
@Autowired(required = false)
private QuartzModuleProperties quartzModuleProperties;
@Bean
@SuppressWarnings("unchecked")
public JobDetailFactoryBean jobDetail() throws ClassNotFoundException {
Class<? extends Job> JobClass = (Class<? extends Job>) Class.forName(jobClassname);
return createJobDetail(JobClass);
@@ -83,20 +58,16 @@ public class SchedulerConfig {
return propertiesFactoryBean.getObject();
}
@Bean(name = "jobTrigger")
public SimpleTriggerFactoryBean sampleJobTrigger(@Qualifier("jobDetail") JobDetail jobDetail,
@Value("${job.frequency}") long frequency, @Value("${job.repeatCount}") int repeatCount) {
return createTrigger(jobDetail, frequency, repeatCount);
}
@Bean(name = "scheduler")
public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory,
@Qualifier("jobTrigger") Trigger sampleJobTrigger) throws IOException {
public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory) throws IOException {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setJobFactory(jobFactory);
factory.setQuartzProperties(quartzProperties());
factory.setTriggers(sampleJobTrigger);
Properties mergedProperties = new Properties();
if(quartzModuleProperties != null)
mergedProperties.putAll(quartzModuleProperties.getProperties());
mergedProperties.putAll(quartzProperties());
factory.setQuartzProperties(mergedProperties);
factory.setAutoStartup(false);
return factory;
}
}
}

View File

@@ -1,161 +1,127 @@
package it.fabioformosa.quartzmanager.controllers;
import java.util.Collections;
import java.util.Map;
import javax.annotation.Resource;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.triggers.SimpleTriggerImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.annotations.Api;
import it.fabioformosa.quartzmanager.dto.SchedulerConfigParam;
import it.fabioformosa.quartzmanager.dto.SchedulerDTO;
import it.fabioformosa.quartzmanager.dto.TriggerStatus;
import it.fabioformosa.quartzmanager.enums.SchedulerStates;
import it.fabioformosa.quartzmanager.scheduler.TriggerMonitor;
import it.fabioformosa.quartzmanager.services.SchedulerService;
import org.quartz.*;
import org.quartz.impl.triggers.SimpleTriggerImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.Map;
/**
* This controller provides scheduler info about config and status. It provides
* also methods to set new config and start/stop/resume the scheduler.
*
* @author Fabio.Formosa
*
*/
@RestController
@RequestMapping("/quartz-manager/scheduler")
@Api(value = "scheduler")
public class SchedulerController {
private static final int MILLS_IN_A_DAY = 1000 * 60 * 60 * 24;
private static final int SEC_IN_A_DAY = 60 * 60 * 24;
private final Logger log = LoggerFactory.getLogger(SchedulerController.class);
private final Logger log = LoggerFactory.getLogger(SchedulerController.class);
private SchedulerService schedulerService;
@Resource
private Scheduler scheduler;
public SchedulerController(SchedulerService schedulerService, ConversionService conversionService) {
this.schedulerService = schedulerService;
this.conversionService = conversionService;
}
@Resource
private TriggerMonitor triggerMonitor;
@Resource
private ConversionService conversionService;
private long fromMillsIntervalToTriggerPerDay(long repeatIntervalInMills) {
return (int) Math.ceil(MILLS_IN_A_DAY / repeatIntervalInMills);
@GetMapping("/config")
public SchedulerConfigParam getConfig() throws SchedulerException {
log.debug("SCHEDULER - GET CONFIG params");
SchedulerConfigParam schedulerConfigParam = schedulerService.getOneSimpleTrigger()
.map(SchedulerController::fromSimpleTriggerToSchedulerConfigParam)
.orElse(new SchedulerConfigParam(0, 0, 0));
return schedulerConfigParam;
}
public static SchedulerConfigParam fromSimpleTriggerToSchedulerConfigParam(SimpleTrigger simpleTrigger){
int timesTriggered = simpleTrigger.getTimesTriggered();
int maxCount = simpleTrigger.getRepeatCount() + 1;
long triggersPerDay = SchedulerService.fromMillsIntervalToTriggerPerDay(simpleTrigger.getRepeatInterval());
return new SchedulerConfigParam(triggersPerDay, maxCount, timesTriggered);
}
@GetMapping
public SchedulerDTO getScheduler() {
log.debug("SCHEDULER - GET Scheduler...");
SchedulerDTO schedulerDTO = conversionService.convert(schedulerService.getScheduler(), SchedulerDTO.class);
return schedulerDTO;
}
@GetMapping("/progress")
public TriggerStatus getProgressInfo() throws SchedulerException {
log.trace("SCHEDULER - GET PROGRESS INFO");
TriggerStatus progress = new TriggerStatus();
SimpleTriggerImpl jobTrigger = (SimpleTriggerImpl) schedulerService.getOneSimpleTrigger().get();
if (jobTrigger != null && jobTrigger.getJobKey() != null) {
progress.setJobKey(jobTrigger.getJobKey().getName());
progress.setJobClass(jobTrigger.getClass().getSimpleName());
progress.setTimesTriggered(jobTrigger.getTimesTriggered());
progress.setRepeatCount(jobTrigger.getRepeatCount());
progress.setFinalFireTime(jobTrigger.getFinalFireTime());
progress.setNextFireTime(jobTrigger.getNextFireTime());
progress.setPreviousFireTime(jobTrigger.getPreviousFireTime());
}
private int fromTriggerPerDayToMillsInterval(long triggerPerDay) {
return (int) Math.ceil(Long.valueOf(MILLS_IN_A_DAY) / triggerPerDay); // with ceil the triggerPerDay is a max value
}
return progress;
}
@SuppressWarnings("unused")
private int fromTriggerPerDayToSecInterval(long triggerPerDay) {
return (int) Math.ceil(Long.valueOf(SEC_IN_A_DAY) / triggerPerDay);
}
@GetMapping(value = "/status", produces = "application/json")
public Map<String, String> getStatus() throws SchedulerException {
log.trace("SCHEDULER - GET STATUS");
String schedulerState = "";
if (schedulerService.getScheduler().isShutdown() || !schedulerService.getScheduler().isStarted())
schedulerState = SchedulerStates.STOPPED.toString();
else if (schedulerService.getScheduler().isStarted() && schedulerService.getScheduler().isInStandbyMode())
schedulerState = SchedulerStates.PAUSED.toString();
else
schedulerState = SchedulerStates.RUNNING.toString();
return Collections.singletonMap("data", schedulerState.toLowerCase());
}
@GetMapping("/config")
public SchedulerConfigParam getConfig() throws SchedulerException {
log.debug("SCHEDULER - GET CONFIG params");
@GetMapping("/pause")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void pause() throws SchedulerException {
log.info("SCHEDULER - PAUSE COMMAND");
schedulerService.getScheduler().standby();
}
SimpleTrigger jobTrigger = (SimpleTrigger) scheduler.getTrigger(triggerMonitor.getTrigger().getKey());
int maxCount = jobTrigger.getRepeatCount() + 1;
long triggersPerDay = fromMillsIntervalToTriggerPerDay(jobTrigger.getRepeatInterval());
@GetMapping("/resume")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void resume() throws SchedulerException {
log.info("SCHEDULER - RESUME COMMAND");
schedulerService.getScheduler().start();
}
return new SchedulerConfigParam(triggersPerDay, maxCount);
}
@GetMapping("/run")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void run() throws SchedulerException {
log.info("SCHEDULER - START COMMAND");
schedulerService.getScheduler().start();
}
@GetMapping("/progress")
public TriggerStatus getProgressInfo() throws SchedulerException {
log.trace("SCHEDULER - GET PROGRESS INFO");
TriggerStatus progress = new TriggerStatus();
SimpleTriggerImpl jobTrigger = (SimpleTriggerImpl) scheduler.getTrigger(triggerMonitor.getTrigger().getKey());
if (jobTrigger != null && jobTrigger.getJobKey() != null) {
progress.setJobKey(jobTrigger.getJobKey().getName());
progress.setJobClass(jobTrigger.getClass().getSimpleName());
progress.setTimesTriggered(jobTrigger.getTimesTriggered());
progress.setRepeatCount(jobTrigger.getRepeatCount());
progress.setFinalFireTime(jobTrigger.getFinalFireTime());
progress.setNextFireTime(jobTrigger.getNextFireTime());
progress.setPreviousFireTime(jobTrigger.getPreviousFireTime());
}
return progress;
}
@GetMapping(produces = "application/json")
public Map<String, String> getStatus() throws SchedulerException {
log.trace("SCHEDULER - GET STATUS");
String schedulerState = "";
if (scheduler.isShutdown() || !scheduler.isStarted())
schedulerState = SchedulerStates.STOPPED.toString();
else if (scheduler.isStarted() && scheduler.isInStandbyMode())
schedulerState = SchedulerStates.PAUSED.toString();
else
schedulerState = SchedulerStates.RUNNING.toString();
return Collections.singletonMap("data", schedulerState.toLowerCase());
}
@GetMapping("/pause")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void pause() throws SchedulerException {
log.info("SCHEDULER - PAUSE COMMAND");
scheduler.standby();
}
@PostMapping("/config")
public SchedulerConfigParam postConfig(@RequestBody SchedulerConfigParam config) throws SchedulerException {
log.info("SCHEDULER - NEW CONFIG {}", config);
SimpleTrigger trigger = (SimpleTrigger) triggerMonitor.getTrigger();
TriggerBuilder<SimpleTrigger> triggerBuilder = trigger.getTriggerBuilder();
int intervalInMills = fromTriggerPerDayToMillsInterval(config.getTriggerPerDay());
Trigger newTrigger = triggerBuilder
.withSchedule(
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMilliseconds(intervalInMills)
.withRepeatCount(config.getMaxCount() - 1)
.withMisfireHandlingInstructionNextWithRemainingCount()
)
.build();
scheduler.rescheduleJob(triggerMonitor.getTrigger().getKey(), newTrigger);
triggerMonitor.setTrigger(newTrigger);
return config;
}
@GetMapping("/resume")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void resume() throws SchedulerException {
log.info("SCHEDULER - RESUME COMMAND");
scheduler.start();
}
@GetMapping("/run")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void run() throws SchedulerException {
log.info("SCHEDULER - START COMMAND");
scheduler.start();
}
@GetMapping("/stop")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void stop() throws SchedulerException {
log.info("SCHEDULER - STOP COMMAND");
scheduler.shutdown(true);
}
@GetMapping("/stop")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void stop() throws SchedulerException {
log.info("SCHEDULER - STOP COMMAND");
schedulerService.getScheduler().shutdown(true);
}
}

View File

@@ -0,0 +1,53 @@
package it.fabioformosa.quartzmanager.controllers;
import io.swagger.annotations.Api;
import it.fabioformosa.quartzmanager.dto.SchedulerConfigParam;
import it.fabioformosa.quartzmanager.dto.TriggerDTO;
import it.fabioformosa.quartzmanager.services.SchedulerService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RequestMapping(TriggerController.TRIGGER_CONTROLLER_BASE_URL)
@RestController
@Api(value = "triggers")
public class TriggerController {
static public final String TRIGGER_CONTROLLER_BASE_URL = "/quartz-manager/triggers";
@Value("${quartz-manager.jobClass}")
private String jobClassname;
private SchedulerService schedulerService;
public TriggerController(SchedulerService schedulerService) {
this.schedulerService = schedulerService;
}
@GetMapping("/{name}")
public TriggerDTO getTrigger(@PathVariable String name) throws SchedulerException {
return schedulerService.getTriggerByName(name);
}
@ResponseStatus(HttpStatus.CREATED)
@PostMapping("/{name}")
public TriggerDTO postTrigger(@PathVariable String name, @RequestBody SchedulerConfigParam config) throws SchedulerException, ClassNotFoundException {
log.info("TRIGGER - CREATING a trigger {} {}", name, config);
TriggerDTO newTriggerDTO = schedulerService.scheduleNewTrigger(name, jobClassname, config);
log.info("TRIGGER - CREATED a trigger {}", newTriggerDTO);
return newTriggerDTO;
}
@PutMapping("/{name}")
public TriggerDTO rescheduleTrigger(@PathVariable String name, @RequestBody SchedulerConfigParam config) throws SchedulerException {
log.info("TRIGGER - RESCHEDULING the trigger {} {}", name, config);
TriggerDTO triggerDTO = schedulerService.rescheduleTrigger(name, config);
log.info("TRIGGER - RESCHEDULED the trigger {}", triggerDTO);
return triggerDTO;
}
}

View File

@@ -12,7 +12,6 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping(value = "/quartz-manager/api", produces = MediaType.APPLICATION_JSON_VALUE)
public class UserController {
@GetMapping("/whoami")
public @ResponseBody Object user() {
SecurityContext context = SecurityContextHolder.getContext();
@@ -21,12 +20,12 @@ public class UserController {
return "\"NO_AUTH\"";
}
/**
* JWT Temporary disabled
*
* @author Fabio.Formosa
*
*/
// /**
// * JWT Temporary disabled
// *
// * @author Fabio.Formosa
// *
// */
// @Autowired
// private UserService userService;

View File

@@ -0,0 +1,15 @@
package it.fabioformosa.quartzmanager.converters;
import it.fabioformosa.metamorphosis.core.converters.AbstractBaseConverterToDTO;
import it.fabioformosa.quartzmanager.dto.JobKeyDTO;
import org.quartz.JobKey;
import org.springframework.stereotype.Component;
@Component
public class JobKeyToJobKeyDTO extends AbstractBaseConverterToDTO<JobKey, JobKeyDTO> {
@Override
protected void convert(JobKey source, JobKeyDTO target) {
target.setName(source.getName());
target.setGroup(source.getGroup());
}
}

View File

@@ -0,0 +1,21 @@
package it.fabioformosa.quartzmanager.converters;
import it.fabioformosa.metamorphosis.core.converters.AbstractBaseConverterToDTO;
import it.fabioformosa.quartzmanager.dto.SchedulerDTO;
import lombok.SneakyThrows;
import org.quartz.Scheduler;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.stereotype.Component;
@Component
public class SchedulerToSchedulerDTO extends AbstractBaseConverterToDTO<Scheduler, SchedulerDTO> {
@SneakyThrows
@Override
protected void convert(Scheduler source, SchedulerDTO target) {
target.setName(source.getSchedulerName());
target.setInstanceId(source.getSchedulerInstanceId());
target.setTriggerKeys(source.getTriggerKeys(GroupMatcher.anyTriggerGroup()));
}
}

View File

@@ -0,0 +1,16 @@
package it.fabioformosa.quartzmanager.converters;
import it.fabioformosa.metamorphosis.core.converters.AbstractBaseConverterToDTO;
import it.fabioformosa.quartzmanager.dto.TriggerKeyDTO;
import org.quartz.TriggerKey;
import org.springframework.stereotype.Component;
@Component
public class TriggerKeyToTriggerKeyDTO extends AbstractBaseConverterToDTO<TriggerKey, TriggerKeyDTO> {
@Override
protected void convert(TriggerKey source, TriggerKeyDTO target) {
target.setName(source.getName());
target.setGroup(source.getGroup());
}
}

View File

@@ -0,0 +1,36 @@
package it.fabioformosa.quartzmanager.converters;
import it.fabioformosa.metamorphosis.core.converters.AbstractBaseConverterToDTO;
import it.fabioformosa.quartzmanager.dto.JobKeyDTO;
import it.fabioformosa.quartzmanager.dto.TriggerDTO;
import it.fabioformosa.quartzmanager.dto.TriggerKeyDTO;
import org.quartz.JobKey;
import org.quartz.Trigger;
import org.quartz.TriggerKey;
import org.springframework.stereotype.Component;
@Component
public class TriggerToTriggerDTO extends AbstractBaseConverterToDTO<Trigger, TriggerDTO> {
@Override
protected void convert(Trigger source, TriggerDTO target) {
TriggerKey triggerKey = source.getKey();
TriggerKeyDTO triggerKeyDTO = conversionService.convert(triggerKey, TriggerKeyDTO.class);
target.setTriggerKeyDTO(triggerKeyDTO);
target.setStartTime(source.getStartTime());
target.setDescription(source.getDescription());
target.setEndTime(source.getEndTime());
target.setFinalFireTime(source.getFinalFireTime());
target.setMisfireInstruction(source.getMisfireInstruction());
target.setNextFireTime(source.getNextFireTime());
target.setPriority(source.getPriority());
target.setMayFireAgain(source.mayFireAgain());
JobKey jobKey = source.getJobKey();
JobKeyDTO jobKeyDTO = conversionService.convert(jobKey, JobKeyDTO.class);
target.setJobKeyDTO(jobKeyDTO);
}
}

View File

@@ -0,0 +1,15 @@
package it.fabioformosa.quartzmanager.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class JobKeyDTO {
private String name;
private String group;
}

View File

@@ -1,40 +1,16 @@
package it.fabioformosa.quartzmanager.dto;
public class SchedulerConfigParam {
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class SchedulerConfigParam {
public long triggerPerDay;
public int maxCount;
public SchedulerConfigParam() {
super();
}
public SchedulerConfigParam(long triggerPerDay, int maxCount) {
super();
this.triggerPerDay = triggerPerDay;
this.maxCount = maxCount;
}
public int getMaxCount() {
return maxCount;
}
public long getTriggerPerDay() {
return triggerPerDay;
}
public void setMaxCount(int maxCount) {
this.maxCount = maxCount;
}
public void setTriggerPerDay(long triggerPerDay) {
this.triggerPerDay = triggerPerDay;
}
@Override
public String toString() {
return "SchedulerConfigParam [triggerPerDay=" + triggerPerDay
+ ", maxCount=" + maxCount + "]";
}
public int timesTriggered;
}

View File

@@ -0,0 +1,35 @@
package it.fabioformosa.quartzmanager.dto;
import org.quartz.TriggerKey;
import java.util.Set;
public class SchedulerDTO {
private String name;
private String instanceId;
private Set<TriggerKey> triggerKeys;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setInstanceId(String instanceId) {
this.instanceId = instanceId;
}
public String getInstanceId() {
return instanceId;
}
public void setTriggerKeys(Set<TriggerKey> triggerKeys) {
this.triggerKeys = triggerKeys;
}
public Set<TriggerKey> getTriggerKeys() {
return triggerKeys;
}
}

View File

@@ -0,0 +1,25 @@
package it.fabioformosa.quartzmanager.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class TriggerDTO {
private TriggerKeyDTO triggerKeyDTO;
private int priority;
private Date startTime;
private String description;
private Date endTime;
private Date finalFireTime;
private int misfireInstruction;
private Date nextFireTime;
private JobKeyDTO jobKeyDTO;
private boolean mayFireAgain;
}

View File

@@ -0,0 +1,15 @@
package it.fabioformosa.quartzmanager.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class TriggerKeyDTO {
private String name;
private String group;
}

View File

@@ -0,0 +1,114 @@
package it.fabioformosa.quartzmanager.services;
import it.fabioformosa.quartzmanager.common.utils.Try;
import it.fabioformosa.quartzmanager.dto.SchedulerConfigParam;
import it.fabioformosa.quartzmanager.dto.TriggerDTO;
import org.quartz.*;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.core.convert.ConversionService;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class SchedulerService {
public static final int MILLS_IN_A_DAY = 1000 * 60 * 60 * 24;
public static final int SEC_IN_A_DAY = 60 * 60 * 24;
private Scheduler scheduler;
private ConversionService conversionService;
public SchedulerService(Scheduler scheduler, ConversionService conversionService) {
this.scheduler = scheduler;
this.conversionService = conversionService;
}
public static int fromTriggerPerDayToMillsInterval(long triggerPerDay) {
return (int) Math.ceil(Long.valueOf(SchedulerService.MILLS_IN_A_DAY) / triggerPerDay); // with ceil the triggerPerDay is a max value
}
public static int fromTriggerPerDayToSecInterval(long triggerPerDay) {
return (int) Math.ceil(Long.valueOf(SchedulerService.SEC_IN_A_DAY) / triggerPerDay);
}
public static long fromMillsIntervalToTriggerPerDay(long repeatIntervalInMills) {
return (int) Math.ceil(MILLS_IN_A_DAY / repeatIntervalInMills);
}
public Scheduler getScheduler() {
return scheduler;
}
public Optional<TriggerKey> getTriggerByKey(String triggerKeyName) throws SchedulerException {
return scheduler.getTriggerKeys(GroupMatcher.anyGroup()).stream()
.filter(triggerKey -> triggerKey.getName().equals(triggerKeyName))
.findFirst();
}
public Optional<SimpleTrigger> getOneSimpleTrigger() throws SchedulerException {
return getOneTriggerKey()
.map(Try.with(triggerKey -> scheduler.getTrigger(triggerKey)))
.filter(Try::isSuccess).map(Try::getSuccess)
.filter(trigger -> trigger instanceof SimpleTrigger)
.map(trigger -> (SimpleTrigger) trigger);
}
public Optional<TriggerKey> getOneTriggerKey() throws SchedulerException {
return scheduler.getTriggerKeys(GroupMatcher.anyGroup()).stream()
.findFirst();
}
public TriggerDTO getTriggerByName(String name) throws SchedulerException {
Trigger trigger = scheduler.getTrigger(new TriggerKey(name));
return conversionService.convert(trigger, TriggerDTO.class);
}
public TriggerDTO scheduleNewTrigger(String name, String jobClassname, SchedulerConfigParam config) throws SchedulerException, ClassNotFoundException {
Class<? extends Job> jobClass = (Class<? extends Job>) Class.forName(jobClassname);
JobDetail jobDetail = JobBuilder.newJob()
.ofType(jobClass)
.storeDurably(false)
.build();
int intervalInMills = SchedulerService.fromTriggerPerDayToMillsInterval(config.getTriggerPerDay());
Trigger newTrigger = TriggerBuilder.newTrigger()
.withSchedule(
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMilliseconds(intervalInMills)
.withRepeatCount(config.getMaxCount() - 1)
.withMisfireHandlingInstructionNextWithRemainingCount()
)
.withIdentity(name)
.build();
scheduler.scheduleJob(jobDetail, newTrigger);
return conversionService.convert(newTrigger, TriggerDTO.class);
}
public TriggerDTO rescheduleTrigger(String name, SchedulerConfigParam config) throws SchedulerException {
int intervalInMills = SchedulerService.fromTriggerPerDayToMillsInterval(config.getTriggerPerDay());
Optional<TriggerKey> optionalTriggerKey = getTriggerByKey(name);
TriggerKey triggerKey = optionalTriggerKey.orElse(TriggerKey.triggerKey(name));
Trigger trigger = scheduler.getTrigger(triggerKey);
Trigger newTrigger = TriggerBuilder.newTrigger()
.withSchedule(
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMilliseconds(intervalInMills)
.withRepeatCount(config.getMaxCount() - 1)
.withMisfireHandlingInstructionNextWithRemainingCount()
)
.forJob(trigger.getJobKey().getName())
.withIdentity(name)
.build();
scheduler.rescheduleJob(triggerKey, newTrigger);
return conversionService.convert(newTrigger, TriggerDTO.class);
}
}

View File

@@ -0,0 +1,13 @@
package it.fabioformosa.quartzmanager;
import org.junit.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class QuartManagerApplicationTests {
@Test
public void contextLoads() {
}
}

View File

@@ -0,0 +1,54 @@
package it.fabioformosa.quartzmanager.controllers;
import it.fabioformosa.quartzmanager.controllers.utils.TestUtils;
import it.fabioformosa.quartzmanager.controllers.utils.TriggerUtils;
import it.fabioformosa.quartzmanager.dto.SchedulerConfigParam;
import it.fabioformosa.quartzmanager.dto.TriggerDTO;
import it.fabioformosa.quartzmanager.services.SchedulerService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import static org.mockito.ArgumentMatchers.any;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@Disabled
@WebMvcTest(controllers = TriggerController.class, properties = {
"quartz-manager.jobClass=it.fabioformosa.quartzmanager.jobs.myjobs.SampleJob"
})
class TriggerControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private SchedulerService schedulerService;
@AfterEach
void cleanUp(){
Mockito.reset(schedulerService);
}
@Test
void givenASchedulerConfigParam_whenPosted_thenANewTriggerIsCreated() throws Exception {
TriggerDTO expectedTriggerDTO = TriggerUtils.getTriggerInstance();
Mockito.when(schedulerService.scheduleNewTrigger(any(), any(), any())).thenReturn(expectedTriggerDTO);
SchedulerConfigParam configParamToPost = SchedulerConfigParam.builder().maxCount(20).triggerPerDay(20000L).build();
mockMvc.perform(
post(TriggerController.TRIGGER_CONTROLLER_BASE_URL + "mytrigger")
.contentType(MediaType.APPLICATION_JSON)
.content(TestUtils.toJson(configParamToPost))
)
.andExpect(MockMvcResultMatchers.status().isCreated())
.andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(expectedTriggerDTO)))
;
}
}

View File

@@ -0,0 +1,15 @@
package it.fabioformosa.quartzmanager.controllers.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
public class TestUtils {
static public ObjectMapper objectMapper = new ObjectMapper();
@SneakyThrows
static public String toJson(Object object){
return objectMapper.writeValueAsString(object);
};
}

View File

@@ -0,0 +1,33 @@
package it.fabioformosa.quartzmanager.controllers.utils;
import it.fabioformosa.quartzmanager.common.utils.DateUtils;
import it.fabioformosa.quartzmanager.dto.JobKeyDTO;
import it.fabioformosa.quartzmanager.dto.TriggerDTO;
import it.fabioformosa.quartzmanager.dto.TriggerKeyDTO;
import java.time.LocalDateTime;
public class TriggerUtils {
static public TriggerDTO getTriggerInstance(){
return TriggerDTO.builder()
.description("sample trigger")
.endTime(DateUtils.getHoursFromNow(2L))
.finalFireTime(DateUtils.getHoursFromNow(2L))
.jobKeyDTO(JobKeyDTO.builder()
.group("defaultJobGroup")
.name("sampleJob")
.build())
.mayFireAgain(true)
.triggerKeyDTO(TriggerKeyDTO.builder()
.group("defaultTriggerGroup")
.name("sampleTrigger")
.build())
.misfireInstruction(1)
.nextFireTime(DateUtils.getHoursFromNow(1L))
.priority(1)
.startTime(DateUtils.fromLocaleDateTimeToDate(LocalDateTime.now()))
.build();
}
}

View File

@@ -0,0 +1,51 @@
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>3.0.2-SNAPSHOT</version>
</parent>
<artifactId>quartz-manager-starter-persistence</artifactId>
<name>Quartz Manager Starter Security</name>
<description>Persist quartz jobs into a database</description>
<url>https://github.com/fabioformosa/quartz-manager</url>
<properties>
<main.basedir>${basedir}/../..</main.basedir>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-common</artifactId>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</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-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,69 @@
package it.fabioformosa.quartzmanager.persistence;
import it.fabioformosa.quartzmanager.common.properties.QuartzModuleProperties;
import liquibase.integration.spring.SpringLiquibase;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.*;
import javax.sql.DataSource;
@Configuration
@PropertySource("classpath:quartz-persistence.properties")
public class PersistenceConfig {
@Value("${quartz-manager.persistence.quartz.datasource.url}")
private String quartzDatasourceUrl;
@Value("${quartz-manager.persistence.quartz.datasource.user}")
private String quartzDatasourceUser;
@Value("${quartz-manager.persistence.quartz.datasource.password}")
private String quartzDatasourcePassword;
@Data
public class PersistenceDatasourceProps {
private String changeLog;
private String contexts;
}
@Bean
public SpringLiquibase liquibase(PersistenceDatasourceProps persistenceDatasourceProps, DataSource quartzManagerDatasource) {
SpringLiquibase liquibase = new SpringLiquibase();
liquibase.setContexts(persistenceDatasourceProps.getContexts());
liquibase.setChangeLog(persistenceDatasourceProps.getChangeLog());
liquibase.setDataSource(quartzManagerDatasource);
liquibase.setDropFirst(false);
return liquibase;
}
@Bean
@ConfigurationProperties(prefix = "spring.liquibase")
public PersistenceDatasourceProps persistenceDatasourceProps() {
return new PersistenceDatasourceProps();
}
@Bean("quartzPersistenceProperties")
public QuartzModuleProperties persistenceQuartzProps(QuartzPersistencePropConfig quartzPersistencePropConfig) {
QuartzModuleProperties quartzModuleProperties = new QuartzModuleProperties();
quartzModuleProperties.setProperties(quartzPersistencePropConfig.getProperties());
quartzModuleProperties.getProperties().setProperty("org.quartz.dataSource.quartzDataSource.URL", quartzDatasourceUrl);
quartzModuleProperties.getProperties().setProperty("org.quartz.dataSource.quartzDataSource.user", quartzDatasourceUser);
quartzModuleProperties.getProperties().setProperty("org.quartz.dataSource.quartzDataSource.password", quartzDatasourcePassword);
return quartzModuleProperties;
}
@Primary
@Bean
public DataSource quartzManagerDatasource(PersistenceDatasourceProps persistenceDatasourceProps) {
return DataSourceBuilder.create()
.url(quartzDatasourceUrl)
.driverClassName("org.postgresql.Driver")
.username(quartzDatasourceUser)
.password(quartzDatasourcePassword)
.build();
}
}

View File

@@ -0,0 +1,17 @@
package it.fabioformosa.quartzmanager.persistence;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.util.Properties;
@Configuration
@PropertySource("classpath:quartz-persistence.properties")
@ConfigurationProperties(prefix = "spring.quartz")
@Getter @Setter
public class QuartzPersistencePropConfig {
private Properties properties;
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
it.fabioformosa.quartzmanager.persistence.PersistenceConfig

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
<includeAll path="./migrations" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,218 @@
--liquibase formatted sql
/* https://github.com/quartz-scheduler/quartz/blob/master/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore/tables_postgres.sql */
--changeset V202102190034_01 (dbms:postgresql)
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
DROP TABLE IF EXISTS QRTZ_LOCKS;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
DROP TABLE IF EXISTS QRTZ_CALENDARS;
--changeset V202102190034_02 (dbms:postgresql)
CREATE TABLE QRTZ_JOB_DETAILS
(
SCHED_NAME VARCHAR(120) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
IS_DURABLE BOOL NOT NULL,
IS_NONCONCURRENT BOOL NOT NULL,
IS_UPDATE_DATA BOOL NOT NULL,
REQUESTS_RECOVERY BOOL NOT NULL,
JOB_DATA BYTEA NULL,
PRIMARY KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
);
--changeset V202102190034_03 (dbms:postgresql)
CREATE TABLE QRTZ_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
NEXT_FIRE_TIME BIGINT NULL,
PREV_FIRE_TIME BIGINT NULL,
PRIORITY INTEGER NULL,
TRIGGER_STATE VARCHAR(16) NOT NULL,
TRIGGER_TYPE VARCHAR(8) NOT NULL,
START_TIME BIGINT NOT NULL,
END_TIME BIGINT NULL,
CALENDAR_NAME VARCHAR(200) NULL,
MISFIRE_INSTR SMALLINT NULL,
JOB_DATA BYTEA NULL,
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS (SCHED_NAME, JOB_NAME, JOB_GROUP)
);
--changeset V202102190034_04 (dbms:postgresql)
CREATE TABLE QRTZ_SIMPLE_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
REPEAT_COUNT BIGINT NOT NULL,
REPEAT_INTERVAL BIGINT NOT NULL,
TIMES_TRIGGERED BIGINT NOT NULL,
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
);
--changeset V202102190034_05 (dbms:postgresql)
CREATE TABLE QRTZ_CRON_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
CRON_EXPRESSION VARCHAR(120) NOT NULL,
TIME_ZONE_ID VARCHAR(80),
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
);
--changeset V202102190034_06 (dbms:postgresql)
CREATE TABLE QRTZ_SIMPROP_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
STR_PROP_1 VARCHAR(512) NULL,
STR_PROP_2 VARCHAR(512) NULL,
STR_PROP_3 VARCHAR(512) NULL,
INT_PROP_1 INT NULL,
INT_PROP_2 INT NULL,
LONG_PROP_1 BIGINT NULL,
LONG_PROP_2 BIGINT NULL,
DEC_PROP_1 NUMERIC(13, 4) NULL,
DEC_PROP_2 NUMERIC(13, 4) NULL,
BOOL_PROP_1 BOOL NULL,
BOOL_PROP_2 BOOL NULL,
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
);
--changeset V202102190034_07 (dbms:postgresql)
CREATE TABLE QRTZ_BLOB_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
BLOB_DATA BYTEA NULL,
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
);
--changeset V202102190034_08 (dbms:postgresql)
CREATE TABLE QRTZ_CALENDARS
(
SCHED_NAME VARCHAR(120) NOT NULL,
CALENDAR_NAME VARCHAR(200) NOT NULL,
CALENDAR BYTEA NOT NULL,
PRIMARY KEY (SCHED_NAME, CALENDAR_NAME)
);
--changeset V202102190034_09 (dbms:postgresql)
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP)
);
--changeset V202102190034_10 (dbms:postgresql)
CREATE TABLE QRTZ_FIRED_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
ENTRY_ID VARCHAR(95) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
FIRED_TIME BIGINT NOT NULL,
SCHED_TIME BIGINT NOT NULL,
PRIORITY INTEGER NOT NULL,
STATE VARCHAR(16) NOT NULL,
JOB_NAME VARCHAR(200) NULL,
JOB_GROUP VARCHAR(200) NULL,
IS_NONCONCURRENT BOOL NULL,
REQUESTS_RECOVERY BOOL NULL,
PRIMARY KEY (SCHED_NAME, ENTRY_ID)
);
--changeset V202102190034_11 (dbms:postgresql)
CREATE TABLE QRTZ_SCHEDULER_STATE
(
SCHED_NAME VARCHAR(120) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
LAST_CHECKIN_TIME BIGINT NOT NULL,
CHECKIN_INTERVAL BIGINT NOT NULL,
PRIMARY KEY (SCHED_NAME, INSTANCE_NAME)
);
--changeset V202102190034_12 (dbms:postgresql)
CREATE TABLE QRTZ_LOCKS
(
SCHED_NAME VARCHAR(120) NOT NULL,
LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (SCHED_NAME, LOCK_NAME)
);
--changeset V202102190034_13 (dbms:postgresql)
CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY
ON QRTZ_JOB_DETAILS (SCHED_NAME, REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_J_GRP
ON QRTZ_JOB_DETAILS (SCHED_NAME, JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_J
ON QRTZ_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_JG
ON QRTZ_TRIGGERS (SCHED_NAME, JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_C
ON QRTZ_TRIGGERS (SCHED_NAME, CALENDAR_NAME);
CREATE INDEX IDX_QRTZ_T_G
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_T_STATE
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_STATE
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_G_STATE
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME
ON QRTZ_TRIGGERS (SCHED_NAME, NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_GROUP, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME);
CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME, REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_FT_J_G
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_JG
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_T_G
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_FT_TG
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_GROUP);
COMMIT;

View File

@@ -0,0 +1,13 @@
spring.liquibase.change-log=classpath:db/quartz-scheduler/liquibase-changelog-master.xml
spring.liquibase.contexts=default
spring.quartz.job-store-type=jdbc
spring.quartz.initialize-schema=never
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.driver=org.postgresql.Driver
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.maxConnections=5
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
spring.quartz.properties.org.quartz.jobStore.dataSource=quartzDataSource
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.provider=hikaricp
spring.quartz.properties.org.quartz.jobStore.misfireThreshold=1000
# org.quartz.jobStore.isClustered=true
# org.quartz.scheduler.instanceId=AUTO

View File

@@ -1,144 +1,144 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>3.0.2-SNAPSHOT</version>
</parent>
<artifactId>quartz-manager-web-showcase</artifactId>
<packaging>war</packaging>
<name>Quartz Manager Web Showcase</name>
<description>A webapp that imports Quartz Manager API lib and the frontend webjar</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<springfox.version>2.9.2</springfox.version>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-api</artifactId>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-ui</artifactId>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-security</artifactId>
</dependency>
<!-- SPRING -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</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-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- MISC -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
</dependency>
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>spring-mock-mvc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>3.0.2-SNAPSHOT</version>
</parent>
<artifactId>quartz-manager-web-showcase</artifactId>
<packaging>war</packaging>
<name>Quartz Manager Web Showcase</name>
<description>A webapp that imports Quartz Manager API lib and the frontend webjar</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<springfox.version>2.9.2</springfox.version>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-api</artifactId>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-ui</artifactId>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-security</artifactId>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-persistence</artifactId>
</dependency>
<!-- SPRING -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- MISC -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
</dependency>
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>spring-mock-mvc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View File

@@ -8,10 +8,9 @@ import it.fabioformosa.quartzmanager.jobs.entities.LogRecord.LogType;
public class SampleJob extends AbstractLoggingJob {
@Override
public LogRecord doIt(JobExecutionContext jobExecutionContext) {
return new LogRecord(LogType.INFO, "Hello!");
return new LogRecord(LogType.INFO, "Hello!");
}
}

View File

@@ -26,8 +26,15 @@ logging:
org.springframework.security: INFO
org.springframework.boot.autoconfigure.security: INFO
it.fabioformosa: DEBUG
org.quartz: INFO
quartz-manager:
persistence:
quartz:
datasource:
url: "jdbc:postgresql://localhost:5432/quartzmanager"
user: "quartzmanager"
password: "quartzmanager"
security:
login-model:
form-login-enabled: true

View File

@@ -12,6 +12,9 @@
<logger name="it.fabioformosa" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT" />
</logger>
<logger name="it.fabioformosa.quartzmanager.security.helpers.impl.JwtTokenAuthenticationFilter" level="WARN" additivity="false">
<appender-ref ref="STDOUT" />
</logger>
<logger name="org.springframework" level="WARN" additivity="false">
<appender-ref ref="STDOUT" />
</logger>
@@ -19,8 +22,8 @@
<appender-ref ref="STDOUT" />
</logger>
<root level="WARN">
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
</configuration>

View File

@@ -1,3 +1,2 @@
org.quartz.scheduler.instanceName=example
org.quartz.scheduler.instanceId=AUTO
org.quartz.threadPool.threadCount=1
org.quartz.threadPool.threadCount=1

View File

@@ -0,0 +1,52 @@
package it.fabioformosa.quartzmanager.controllers;
import it.fabioformosa.quartzmanager.controllers.utils.TestUtils;
import it.fabioformosa.quartzmanager.controllers.utils.TriggerUtils;
import it.fabioformosa.quartzmanager.dto.SchedulerConfigParam;
import it.fabioformosa.quartzmanager.dto.TriggerDTO;
import it.fabioformosa.quartzmanager.services.SchedulerService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import static org.mockito.ArgumentMatchers.any;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@WebMvcTest(controllers = TriggerController.class, properties = {
"quartz-manager.jobClass=it.fabioformosa.quartzmanager.jobs.myjobs.SampleJob"
})
class TriggerControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private SchedulerService schedulerService;
@AfterEach
void cleanUp(){
Mockito.reset(schedulerService);
}
@Test
void givenASchedulerConfigParam_whenPosted_thenANewTriggerIsCreated() throws Exception {
TriggerDTO expectedTriggerDTO = TriggerUtils.getTriggerInstance();
Mockito.when(schedulerService.scheduleNewTrigger(any(), any(), any())).thenReturn(expectedTriggerDTO);
SchedulerConfigParam configParamToPost = SchedulerConfigParam.builder().maxCount(20).triggerPerDay(20000L).build();
mockMvc.perform(
post(TriggerController.TRIGGER_CONTROLLER_BASE_URL + "mytrigger")
.contentType(MediaType.APPLICATION_JSON)
.content(TestUtils.toJson(configParamToPost))
)
.andExpect(MockMvcResultMatchers.status().isCreated())
.andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(expectedTriggerDTO)))
;
}
}

View File

@@ -0,0 +1,15 @@
package it.fabioformosa.quartzmanager.controllers.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
public class TestUtils {
static public ObjectMapper objectMapper = new ObjectMapper();
@SneakyThrows
static public String toJson(Object object){
return objectMapper.writeValueAsString(object);
};
}

View File

@@ -0,0 +1,33 @@
package it.fabioformosa.quartzmanager.controllers.utils;
import it.fabioformosa.quartzmanager.common.utils.DateUtils;
import it.fabioformosa.quartzmanager.dto.JobKeyDTO;
import it.fabioformosa.quartzmanager.dto.TriggerDTO;
import it.fabioformosa.quartzmanager.dto.TriggerKeyDTO;
import java.time.LocalDateTime;
public class TriggerUtils {
static public TriggerDTO getTriggerInstance(){
return TriggerDTO.builder()
.description("sample trigger")
.endTime(DateUtils.getHoursFromNow(2L))
.finalFireTime(DateUtils.getHoursFromNow(2L))
.jobKeyDTO(JobKeyDTO.builder()
.group("defaultJobGroup")
.name("sampleJob")
.build())
.mayFireAgain(true)
.triggerKeyDTO(TriggerKeyDTO.builder()
.group("defaultTriggerGroup")
.name("sampleTrigger")
.build())
.misfireInstruction(1)
.nextFireTime(DateUtils.getHoursFromNow(1L))
.priority(1)
.startTime(DateUtils.fromLocaleDateTimeToDate(LocalDateTime.now()))
.build();
}
}