Merge pull request #50 from fabioformosa/develop

Develop
This commit is contained in:
Fabio Formosa
2021-11-02 23:03:32 +01:00
committed by GitHub
50 changed files with 1652 additions and 634 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

@@ -29,6 +29,7 @@
"@fortawesome/fontawesome-free-regular": "^5.0.8",
"@fortawesome/fontawesome-free-solid": "^5.0.8",
"@stomp/ng2-stompjs": "^0.6.3",
"@types/jest": "^27.0.2",
"core-js": "2.5.1",
"hammerjs": "2.0.8",
"net": "^1.0.2",
@@ -65,6 +66,8 @@
},
"jest": {
"preset": "jest-preset-angular",
"setupFilesAfterEnv": ["<rootDir>/jest.setup.ts"]
"setupFilesAfterEnv": [
"<rootDir>/jest.setup.ts"
]
}
}

View File

@@ -2,36 +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 EXISTING COUNT</div>
<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 left unchanged (missed events are definitively lost).
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 the end-time of the trigger has arrived,
so this misfire instruction doesn't guarantee that the repeat counter reaches your max value, but it guarantees that the end-time doesn't go over the expected final fire time.
<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,105 @@
import { TestBed, async, inject } from '@angular/core/testing';
import { Router } from '@angular/router';
import { NO_AUTH, UserService } from '../services';
import { AdminGuard } from './admin.guard';
import {jest} from '@jest/globals'
export class RouterStub {
navigate(commands?: any[], extras?: any) {}
}
const RouterSpy = jest.spyOn(RouterStub.prototype, 'navigate');
const MockUserServiceNoAuth = jest.fn(() => ({currentUser: NO_AUTH}));
const MockUserService = jest.fn(() => ({
currentUser: {
authorities: ['ROLE_ADMIN']
}
}));
const MockUserServiceForbidden = jest.fn(() => ({
currentUser: {
authorities: ['ROLE_GUEST']
}
}));
// describe('AdminGuard NoAuth', () => {
// beforeEach(() => {
// TestBed.configureTestingModule({
// providers: [
// AdminGuard,
// {
// provide: Router,
// useClass: RouterStub
// },
// {
// provide: UserService,
// useClass: MockUserServiceNoAuth
// }
// ]
// });
// });
//
// test.skip('should run', inject([AdminGuard], (guard: AdminGuard) => {
// expect(guard).toBeTruthy();
// }));
//
// test.skip('returns true if user is NO_AUTH', inject([AdminGuard], (guard: AdminGuard) => {
// expect(guard.canActivate(null, null)).toBeTruthy();
// }));
//
// });
// describe('AdminGuard activates the route', () => {
// beforeEach(() => {
// TestBed.configureTestingModule({
// providers: [
// AdminGuard,
// {
// provide: Router,
// useClass: RouterStub
// },
// {
// provide: UserService,
// useClass: MockUserService
// }
// ]
// });
// });
//
// test.skip('should run', inject([AdminGuard], (guard: AdminGuard) => {
// expect(guard).toBeTruthy();
// }));
//
// test.skip('returns true if user has admin role', inject([AdminGuard], (guard: AdminGuard) => {
// expect(guard.canActivate(null, null)).toBeTruthy();
// }));
//
// });
// describe('AdminGuard redirects to 403', () => {
// beforeEach(() => {
// TestBed.configureTestingModule({
// providers: [
// AdminGuard,
// {
// provide: Router,
// useClass: RouterStub
// },
// {
// provide: UserService,
// useClass: MockUserServiceForbidden
// }
// ]
// });
// });
//
// test.skip('should run', inject([AdminGuard], (guard: AdminGuard) => {
// expect(guard).toBeTruthy();
// }));
//
// test.skip('returns false if user is not authorized', inject([AdminGuard], (guard: AdminGuard) => {
// expect(guard.canActivate(null, null)).toBeFalsy();
// expect(RouterSpy).toHaveBeenCalledTimes(1);
// }));
//
// });

View File

@@ -1,105 +0,0 @@
import { TestBed, async, inject } from '@angular/core/testing';
import { Router } from '@angular/router';
import { NO_AUTH, UserService } from '../services';
import { AdminGuard } from './admin.guard';
import {jest} from '@jest/globals'
export class RouterStub {
navigate(commands?: any[], extras?: any) {}
}
const RouterSpy = jest.spyOn(RouterStub.prototype, 'navigate');
const MockUserServiceNoAuth = jest.fn(() => ({currentUser: NO_AUTH}));
const MockUserService = jest.fn(() => ({
currentUser: {
authorities: ['ROLE_ADMIN']
}
}));
const MockUserServiceForbidden = jest.fn(() => ({
currentUser: {
authorities: ['ROLE_GUEST']
}
}));
describe('AdminGuard NoAuth', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
AdminGuard,
{
provide: Router,
useClass: RouterStub
},
{
provide: UserService,
useClass: MockUserServiceNoAuth
}
]
});
});
it('should run', inject([AdminGuard], (guard: AdminGuard) => {
expect(guard).toBeTruthy();
}));
it('returns true if user is NO_AUTH',inject([AdminGuard], (guard: AdminGuard) => {
expect(guard.canActivate(null, null)).toBeTruthy();
}));
});
describe('AdminGuard activates the route', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
AdminGuard,
{
provide: Router,
useClass: RouterStub
},
{
provide: UserService,
useClass: MockUserService
}
]
});
});
it('should run', inject([AdminGuard], (guard: AdminGuard) => {
expect(guard).toBeTruthy();
}));
it('returns true if user has admin role',inject([AdminGuard], (guard: AdminGuard) => {
expect(guard.canActivate(null, null)).toBeTruthy();
}));
});
describe('AdminGuard redirects to 403', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
AdminGuard,
{
provide: Router,
useClass: RouterStub
},
{
provide: UserService,
useClass: MockUserServiceForbidden
}
]
});
});
it('should run', inject([AdminGuard], (guard: AdminGuard) => {
expect(guard).toBeTruthy();
}));
it('returns false if user is not authorized',inject([AdminGuard], (guard: AdminGuard) => {
expect(guard.canActivate(null, null)).toBeFalsy();
expect(RouterSpy).toHaveBeenCalledTimes(1);
}));
});

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,11 +15,9 @@ 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
@@ -37,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(SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT);// 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);
@@ -81,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,154 +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() {
log.debug("SCHEDULER - GET CONFIG params");
SimpleTrigger simpleTrigger = (SimpleTrigger) triggerMonitor.getTrigger();
@GetMapping("/pause")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void pause() throws SchedulerException {
log.info("SCHEDULER - PAUSE COMMAND");
schedulerService.getScheduler().standby();
}
int maxCount = simpleTrigger.getRepeatCount() + 1;
long triggersPerDay = fromMillsIntervalToTriggerPerDay(simpleTrigger.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)).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,151 @@
<?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>
<!-- TEST -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</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