diff --git a/quartz-manager-frontend/eslint.sonar.config.mjs b/quartz-manager-frontend/eslint.sonar.config.mjs new file mode 100644 index 0000000..e4e69ed --- /dev/null +++ b/quartz-manager-frontend/eslint.sonar.config.mjs @@ -0,0 +1,30 @@ +import sonarjs from 'eslint-plugin-sonarjs'; +import tsParser from '@typescript-eslint/parser'; + +export default [ + { + files: ['src/**/*.ts'], + languageOptions: { + parser: tsParser, + parserOptions: { + project: 'tsconfig.json', + sourceType: 'module' + } + }, + plugins: { + sonarjs + }, + rules: { + ...sonarjs.configs.recommended.rules, + 'sonarjs/deprecation': 'off', + 'sonarjs/no-commented-code': 'off', + 'sonarjs/no-dead-store': 'off', + 'sonarjs/no-incomplete-assertions': 'off', + 'sonarjs/no-primitive-wrappers': 'off', + 'sonarjs/no-unused-vars': 'off', + 'sonarjs/prefer-promise-shorthand': 'off', + 'sonarjs/todo-tag': 'off', + 'sonarjs/unused-import': 'off' + } + } +]; diff --git a/quartz-manager-frontend/package.json b/quartz-manager-frontend/package.json index f7629f6..e78abfd 100644 --- a/quartz-manager-frontend/package.json +++ b/quartz-manager-frontend/package.json @@ -8,8 +8,8 @@ "build": "ng build --configuration production", "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", "lint": "ng lint", - "lint:sonar": "eslint --no-eslintrc -c .eslintrc.sonar.json \"src/**/*.ts\"", - "lint:sonar:fix": "eslint --no-eslintrc -c .eslintrc.sonar.json \"src/**/*.ts\" --fix" + "lint:sonar": "eslint -c eslint.sonar.config.mjs \"src/**/*.ts\"", + "lint:sonar:fix": "eslint -c eslint.sonar.config.mjs \"src/**/*.ts\" --fix" }, "private": true, "dependencies": { diff --git a/quartz-manager-frontend/src/app/components/scheduler-control/scheduler-control.component.ts b/quartz-manager-frontend/src/app/components/scheduler-control/scheduler-control.component.ts index 533940a..ea01c70 100644 --- a/quartz-manager-frontend/src/app/components/scheduler-control/scheduler-control.component.ts +++ b/quartz-manager-frontend/src/app/components/scheduler-control/scheduler-control.component.ts @@ -35,16 +35,16 @@ export class SchedulerControlComponent implements OnInit { }); }; - stopScheduler = function () { - this.schedulerService.stopScheduler().subscribe((res) => { - this.scheduler.status = 'STOPPED' + stopScheduler = function () { + this.schedulerService.shutdownScheduler().subscribe((res) => { + this.scheduler.status = 'STOPPED' }, (res) => { console.log(JSON.stringify(res)) }); }; - pauseScheduler = function () { - this.schedulerService.pauseScheduler().subscribe((res) => { + pauseScheduler = function () { + this.schedulerService.standbyScheduler().subscribe((res) => { this.scheduler.status = 'PAUSED' }, (res) => { console.log(JSON.stringify(res)) diff --git a/quartz-manager-frontend/src/app/model/scheduled-job.model.ts b/quartz-manager-frontend/src/app/model/scheduled-job.model.ts new file mode 100644 index 0000000..ae9ee88 --- /dev/null +++ b/quartz-manager-frontend/src/app/model/scheduled-job.model.ts @@ -0,0 +1,12 @@ +import {JobKeyModel} from './jobKey.model'; +import {TriggerKey} from './triggerKey.model'; + +export class ScheduledJob { + jobKeyDTO: JobKeyModel; + jobClassName: string; + description: string; + durable: boolean; + requestsRecovery: boolean; + jobDataMap: {[key: string]: unknown}; + triggerKeys: TriggerKey[]; +} diff --git a/quartz-manager-frontend/src/app/model/scheduler.model.ts b/quartz-manager-frontend/src/app/model/scheduler.model.ts index e7df13a..780da6b 100644 --- a/quartz-manager-frontend/src/app/model/scheduler.model.ts +++ b/quartz-manager-frontend/src/app/model/scheduler.model.ts @@ -5,6 +5,14 @@ export class Scheduler { instanceId: string; status: string; triggerKeys: TriggerKey[]; + quartzVersion: string; + jobStoreClass: string; + jobStoreSupportsPersistence: boolean; + clustered: boolean; + threadPoolClass: string; + threadPoolSize: number; + runningSince: string; + numberOfJobsExecuted: number; constructor(name: string, instanceId: string, status: string, triggerKeys: TriggerKey[]) { this.name = name; diff --git a/quartz-manager-frontend/src/app/model/simple-trigger.command.ts b/quartz-manager-frontend/src/app/model/simple-trigger.command.ts index 8df39d4..5c91b62 100644 --- a/quartz-manager-frontend/src/app/model/simple-trigger.command.ts +++ b/quartz-manager-frontend/src/app/model/simple-trigger.command.ts @@ -1,5 +1,6 @@ export class SimpleTriggerCommand { triggerName: string; + triggerGroup: string; jobClass: string; startDate: Date; endDate: Date; diff --git a/quartz-manager-frontend/src/app/model/trigger.model.ts b/quartz-manager-frontend/src/app/model/trigger.model.ts index 8d4db4b..0edecd0 100644 --- a/quartz-manager-frontend/src/app/model/trigger.model.ts +++ b/quartz-manager-frontend/src/app/model/trigger.model.ts @@ -11,6 +11,10 @@ export class Trigger { finalFireTime: Date; misfireInstruction: number; nextFireTime: Date; + previousFireTime: Date; + type: string; + state: string; + calendarName: string; jobKeyDTO: JobKeyModel; jobDetailDTO: JobDetail = new JobDetail(); mayFireAgain: boolean; diff --git a/quartz-manager-frontend/src/app/services/job.service.spec.ts b/quartz-manager-frontend/src/app/services/job.service.spec.ts new file mode 100644 index 0000000..70aba92 --- /dev/null +++ b/quartz-manager-frontend/src/app/services/job.service.spec.ts @@ -0,0 +1,32 @@ +import JobService from './job.service'; +import {ScheduledJob} from '../model/scheduled-job.model'; +import {jest} from '@jest/globals'; + +describe('JobService', () => { + let apiService: any; + let jobService: JobService; + + beforeEach(() => { + apiService = { + get: jest.fn(), + post: jest.fn(), + delete: jest.fn() + }; + jobService = new JobService(apiService); + }); + + it('uses job class and scheduled job endpoints', () => { + const job = new ScheduledJob(); + job.jobKeyDTO = {group: 'DEFAULT', name: 'sampleJob'}; + + jobService.fetchJobs(); + jobService.fetchScheduledJobs(); + jobService.triggerJob(job); + jobService.deleteJob(job); + + expect(apiService.get).toHaveBeenCalledWith('/quartz-manager/job-classes'); + expect(apiService.get).toHaveBeenCalledWith('/quartz-manager/jobs'); + expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/jobs/DEFAULT/sampleJob/trigger', {}); + expect(apiService.delete).toHaveBeenCalledWith('/quartz-manager/jobs/DEFAULT/sampleJob'); + }); +}); diff --git a/quartz-manager-frontend/src/app/services/job.service.ts b/quartz-manager-frontend/src/app/services/job.service.ts index 037ed9a..7dcd5e7 100644 --- a/quartz-manager-frontend/src/app/services/job.service.ts +++ b/quartz-manager-frontend/src/app/services/job.service.ts @@ -2,6 +2,7 @@ import {Injectable} from '@angular/core'; import {ApiService} from './api.service'; import {CONTEXT_PATH, getBaseUrl} from './config.service'; import {Observable} from 'rxjs'; +import {ScheduledJob} from '../model/scheduled-job.model'; @Injectable() export default class JobService { @@ -12,7 +13,19 @@ export default class JobService { } fetchJobs = (): Observable => { + return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/job-classes`) + } + + fetchScheduledJobs = (): Observable => { return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/jobs`) } + triggerJob = (job: ScheduledJob): Observable => { + return this.apiService.post(getBaseUrl() + `${CONTEXT_PATH}/jobs/${job.jobKeyDTO.group}/${job.jobKeyDTO.name}/trigger`, {}) + } + + deleteJob = (job: ScheduledJob): Observable => { + return this.apiService.delete(getBaseUrl() + `${CONTEXT_PATH}/jobs/${job.jobKeyDTO.group}/${job.jobKeyDTO.name}`) + } + } diff --git a/quartz-manager-frontend/src/app/services/scheduler.service.spec.ts b/quartz-manager-frontend/src/app/services/scheduler.service.spec.ts new file mode 100644 index 0000000..56a21f8 --- /dev/null +++ b/quartz-manager-frontend/src/app/services/scheduler.service.spec.ts @@ -0,0 +1,43 @@ +import {SchedulerService} from './scheduler.service'; +import {SimpleTriggerCommand} from '../model/simple-trigger.command'; +import {jest} from '@jest/globals'; + +describe('SchedulerService', () => { + let apiService: any; + let schedulerService: SchedulerService; + + beforeEach(() => { + apiService = { + get: jest.fn(), + post: jest.fn(), + put: jest.fn() + }; + schedulerService = new SchedulerService(apiService); + }); + + it('uses POST scheduler lifecycle endpoints', () => { + schedulerService.startScheduler(); + schedulerService.standbyScheduler(); + schedulerService.resumeScheduler(); + schedulerService.shutdownScheduler(); + + expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/scheduler/start', {}); + expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/scheduler/standby', {}); + expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/scheduler/resume', {}); + expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/scheduler/shutdown', {}); + }); + + it('uses grouped simple trigger endpoints', () => { + const command = new SimpleTriggerCommand(); + command.triggerGroup = 'DEFAULT'; + command.triggerName = 'sampleTrigger'; + + schedulerService.getSimpleTriggerConfig(command.triggerName, command.triggerGroup); + schedulerService.saveSimpleTriggerConfig(command); + schedulerService.updateSimpleTriggerConfig(command); + + expect(apiService.get).toHaveBeenCalledWith('/quartz-manager/simple-triggers/DEFAULT/sampleTrigger'); + expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/simple-triggers/DEFAULT/sampleTrigger', command); + expect(apiService.put).toHaveBeenCalledWith('/quartz-manager/simple-triggers/DEFAULT/sampleTrigger', command); + }); +}); diff --git a/quartz-manager-frontend/src/app/services/scheduler.service.ts b/quartz-manager-frontend/src/app/services/scheduler.service.ts index e675862..1212e4c 100644 --- a/quartz-manager-frontend/src/app/services/scheduler.service.ts +++ b/quartz-manager-frontend/src/app/services/scheduler.service.ts @@ -14,20 +14,20 @@ export class SchedulerService { private apiService: ApiService ) { } - startScheduler = (): Observable => { - return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/scheduler/run`); - } - - stopScheduler = (): Observable => { - return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/scheduler/stop`); - } - - pauseScheduler = (): Observable => { - return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/scheduler/pause`); - } - - resumeScheduler = (): Observable => { - return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/scheduler/resume`); + startScheduler = (): Observable => { + return this.apiService.post(getBaseUrl() + `${CONTEXT_PATH}/scheduler/start`, {}); + } + + shutdownScheduler = (): Observable => { + return this.apiService.post(getBaseUrl() + `${CONTEXT_PATH}/scheduler/shutdown`, {}); + } + + standbyScheduler = (): Observable => { + return this.apiService.post(getBaseUrl() + `${CONTEXT_PATH}/scheduler/standby`, {}); + } + + resumeScheduler = (): Observable => { + return this.apiService.post(getBaseUrl() + `${CONTEXT_PATH}/scheduler/resume`, {}); } getStatus = () => { @@ -38,17 +38,17 @@ export class SchedulerService { return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/scheduler`); } - getSimpleTriggerConfig = (triggerName: string): Observable => { - return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/simple-triggers/${triggerName}`); - } - - saveSimpleTriggerConfig = (config: SimpleTriggerCommand) => { - return this.apiService.post(getBaseUrl() + `${CONTEXT_PATH}/simple-triggers/${config.triggerName}`, config) - } - - updateSimpleTriggerConfig = (config: SimpleTriggerCommand) => { - return this.apiService.put(getBaseUrl() + `${CONTEXT_PATH}/simple-triggers/${config.triggerName}`, config) - } + getSimpleTriggerConfig = (triggerName: string, triggerGroup = 'DEFAULT'): Observable => { + return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/simple-triggers/${triggerGroup}/${triggerName}`); + } + + saveSimpleTriggerConfig = (config: SimpleTriggerCommand) => { + return this.apiService.post(getBaseUrl() + `${CONTEXT_PATH}/simple-triggers/${config.triggerGroup}/${config.triggerName}`, config) + } + + updateSimpleTriggerConfig = (config: SimpleTriggerCommand) => { + return this.apiService.put(getBaseUrl() + `${CONTEXT_PATH}/simple-triggers/${config.triggerGroup}/${config.triggerName}`, config) + } } diff --git a/quartz-manager-frontend/src/app/services/trigger.service.spec.ts b/quartz-manager-frontend/src/app/services/trigger.service.spec.ts new file mode 100644 index 0000000..648be7d --- /dev/null +++ b/quartz-manager-frontend/src/app/services/trigger.service.spec.ts @@ -0,0 +1,31 @@ +import {TriggerService} from './trigger.service'; +import {TriggerKey} from '../model/triggerKey.model'; +import {jest} from '@jest/globals'; + +describe('TriggerService', () => { + let apiService: any; + let triggerService: TriggerService; + + beforeEach(() => { + apiService = { + get: jest.fn(), + post: jest.fn(), + delete: jest.fn() + }; + triggerService = new TriggerService(apiService); + }); + + it('uses grouped trigger lifecycle endpoints', () => { + const triggerKey = new TriggerKey('sampleTrigger', 'DEFAULT'); + + triggerService.getTrigger(triggerKey); + triggerService.pauseTrigger(triggerKey); + triggerService.resumeTrigger(triggerKey); + triggerService.unscheduleTrigger(triggerKey); + + expect(apiService.get).toHaveBeenCalledWith('/quartz-manager/triggers/DEFAULT/sampleTrigger'); + expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/triggers/DEFAULT/sampleTrigger/pause', {}); + expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/triggers/DEFAULT/sampleTrigger/resume', {}); + expect(apiService.delete).toHaveBeenCalledWith('/quartz-manager/triggers/DEFAULT/sampleTrigger'); + }); +}); diff --git a/quartz-manager-frontend/src/app/services/trigger.service.ts b/quartz-manager-frontend/src/app/services/trigger.service.ts index 9aed968..f8329b8 100644 --- a/quartz-manager-frontend/src/app/services/trigger.service.ts +++ b/quartz-manager-frontend/src/app/services/trigger.service.ts @@ -16,5 +16,20 @@ export class TriggerService { return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/triggers`); } + getTrigger = (triggerKey: TriggerKey): Observable => { + return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/triggers/${triggerKey.group || 'DEFAULT'}/${triggerKey.name}`); + } + + pauseTrigger = (triggerKey: TriggerKey): Observable => { + return this.apiService.post(getBaseUrl() + `${CONTEXT_PATH}/triggers/${triggerKey.group || 'DEFAULT'}/${triggerKey.name}/pause`, {}); + } + + resumeTrigger = (triggerKey: TriggerKey): Observable => { + return this.apiService.post(getBaseUrl() + `${CONTEXT_PATH}/triggers/${triggerKey.group || 'DEFAULT'}/${triggerKey.name}/resume`, {}); + } + + unscheduleTrigger = (triggerKey: TriggerKey): Observable => { + return this.apiService.delete(getBaseUrl() + `${CONTEXT_PATH}/triggers/${triggerKey.group || 'DEFAULT'}/${triggerKey.name}`); + } } diff --git a/quartz-manager-frontend/src/app/views/manager/manager.component.html b/quartz-manager-frontend/src/app/views/manager/manager.component.html index 72623a7..2494a5e 100644 --- a/quartz-manager-frontend/src/app/views/manager/manager.component.html +++ b/quartz-manager-frontend/src/app/views/manager/manager.component.html @@ -47,7 +47,7 @@ {{ scheduler?.status || 'LOADING' }}
Instance ID{{ scheduler?.instanceId || '-' }}
- +
Cluster{{ scheduler?.clustered ? 'YES' : 'NO' }}
WebSocketOPEN
@@ -79,7 +79,7 @@
{{ scheduler?.status || '-' }}
{{ triggerKeys.length }}
{{ jobs.length }}
- +
{{ scheduler?.quartzVersion || '-' }}
@@ -119,12 +119,12 @@
{{ selectedTrigger?.timesTriggered ? 'tracked by progress events' : 'not exposed' }}
{{ formatDateTime(selectedTrigger?.nextFireTime) || '-' }}
{{ selectedTrigger?.priority || '-' }}
-
Roadmap
+
{{ selectedTrigger?.calendarName || 'none' }}
{{ selectedTrigger?.misfireInstruction || '-' }}
{{ getSelectedTriggerRepeatSummary() }}
Current run progress
{{ getProgressLabel() }}
-
+
} @else {
EMPTY

No trigger selected

Create a SimpleTrigger or refresh trigger keys.
} @@ -163,36 +163,38 @@
-

Jobs

The current backend exposes eligible Quartz Manager job classes. Full job registry metadata, CRUD, durability, recovery, and group operations remain roadmap features.

+

Jobs

The backend exposes scheduled Quartz jobs plus eligible job classes for SimpleTrigger creation. Durability, recovery, data map, and related trigger keys are read-only in this release.

-

Eligible Job Classes

{{ jobs.length }} JOBS
+

Scheduled Jobs

{{ scheduledJobs.length }} JOBS
- @for (jobClass of getJobClassRows(); track jobClass) { - + @for (job of getScheduledJobRows(); track job.jobKeyDTO.group + '.' + job.jobKeyDTO.name) { + + } @empty { + }
Job keyClassDurableRecoveryTriggers
{{ shortClassName(jobClass) }}{{ jobClass }}RoadmapRoadmapRoadmap
{{ job.jobKeyDTO.group }}.{{ job.jobKeyDTO.name }}{{ job.jobClassName }}{{ job.durable ? 'YES' : 'NO' }}{{ job.requestsRecovery ? 'YES' : 'NO' }}{{ job.triggerKeys?.length || 0 }}
No scheduled jobs returned by the backend. Create a SimpleTrigger from an eligible job class.
@@ -227,11 +229,11 @@ Returns eligible Java job class names.
{{ formatDateTime(selectedTrigger?.finalFireTime) || 'none' }}
{{ selectedTrigger?.repeatInterval ? formatDuration(selectedTrigger.repeatInterval) : '-' }}
- +
{{ selectedTrigger?.calendarName || 'none' }}

Schedule summary

{{ getSelectedTriggerRepeatSummary() }}. Next fire: {{ formatDateTime(selectedTrigger?.nextFireTime) || 'not available' }}.
-
-
Danger zoneUnschedule and reset-error require backend endpoints that are still on the roadmap.
+
+
Danger zoneUnschedule uses the trigger lifecycle endpoint. Reset-error remains roadmap-gated.
@@ -307,7 +309,7 @@ Returns eligible Java job class names.

Scheduler Metadata

CURRENT API
-
{{ scheduler?.name || '-' }}
{{ scheduler?.instanceId || '-' }}
{{ scheduler?.status || '-' }}
{{ triggerKeys.length }}
+
{{ scheduler?.name || '-' }}
{{ scheduler?.instanceId || '-' }}
{{ scheduler?.status || '-' }}
{{ triggerKeys.length }}
{{ scheduler?.quartzVersion || '-' }}
{{ scheduler?.threadPoolSize || '-' }}
{{ scheduler?.jobStoreClass || '-' }}
{{ scheduler?.clustered ? 'YES' : 'NO' }}

Cluster Nodes

ROADMAP
diff --git a/quartz-manager-frontend/src/app/views/manager/manager.component.ts b/quartz-manager-frontend/src/app/views/manager/manager.component.ts index 2bbda83..2827ec3 100644 --- a/quartz-manager-frontend/src/app/views/manager/manager.component.ts +++ b/quartz-manager-frontend/src/app/views/manager/manager.component.ts @@ -9,6 +9,7 @@ import {ProgressRxWebsocketService} from '../../services/progress.rx-websocket.s import {Scheduler} from '../../model/scheduler.model'; import {SimpleTriggerCommand} from '../../model/simple-trigger.command'; import {SimpleTrigger} from '../../model/simple-trigger.model'; +import {ScheduledJob} from '../../model/scheduled-job.model'; import {TriggerKey} from '../../model/triggerKey.model'; import TriggerFiredBundle from '../../model/trigger-fired-bundle.model'; @@ -54,7 +55,9 @@ export class ManagerComponent implements OnInit, OnDestroy { selectedTriggerKey: TriggerKey; selectedTrigger: SimpleTrigger; selectedJobClass: string; + selectedScheduledJob: ScheduledJob; jobs: string[] = []; + scheduledJobs: ScheduledJob[] = []; logs: ConsoleLogRecord[] = []; progress: TriggerFiredBundle; roadmapNotice: string; @@ -87,6 +90,7 @@ export class ManagerComponent implements OnInit, OnDestroy { this.refreshScheduler(); this.fetchTriggers(); this.fetchJobs(); + this.fetchScheduledJobs(); } ngOnDestroy() { @@ -179,7 +183,7 @@ export class ManagerComponent implements OnInit, OnDestroy { } standbyScheduler() { - const subscription = this.schedulerService.pauseScheduler().subscribe({ + const subscription = this.schedulerService.standbyScheduler().subscribe({ next: () => this.setSchedulerStatus('PAUSED', 'Scheduler moved to standby.'), error: () => this.operationError = 'Unable to move the scheduler to standby.' }); @@ -198,7 +202,7 @@ export class ManagerComponent implements OnInit, OnDestroy { if (!window.confirm('Shutdown the scheduler instance?')) { return; } - const subscription = this.schedulerService.stopScheduler().subscribe({ + const subscription = this.schedulerService.shutdownScheduler().subscribe({ next: () => this.setSchedulerStatus('STOPPED', 'Scheduler shut down.'), error: () => this.operationError = 'Unable to shut down the scheduler.' }); @@ -243,12 +247,23 @@ export class ManagerComponent implements OnInit, OnDestroy { this.subscriptions.push(subscription); } + fetchScheduledJobs() { + const subscription = this.jobService.fetchScheduledJobs().subscribe({ + next: scheduledJobs => { + this.scheduledJobs = scheduledJobs || []; + this.selectedScheduledJob = this.scheduledJobs[0]; + }, + error: () => this.operationError = 'Unable to load scheduled jobs.' + }); + this.subscriptions.push(subscription); + } + fetchTriggerDetails(triggerKeys: TriggerKey[]) { triggerKeys.forEach(triggerKey => { - const subscription = this.schedulerService.getSimpleTriggerConfig(triggerKey.name).subscribe({ - next: trigger => this.triggerDetailsByName[triggerKey.name] = trigger as SimpleTrigger, + const subscription = this.schedulerService.getSimpleTriggerConfig(triggerKey.name, this.getTriggerGroup(triggerKey)).subscribe({ + next: trigger => this.triggerDetailsByName[this.getTriggerDetailKey(triggerKey)] = trigger as SimpleTrigger, error: () => { - this.triggerDetailsByName[triggerKey.name] = null; + this.triggerDetailsByName[this.getTriggerDetailKey(triggerKey)] = null; } }); this.subscriptions.push(subscription); @@ -264,12 +279,12 @@ export class ManagerComponent implements OnInit, OnDestroy { this.openDetailDrawer(); } this.triggerLoading = true; - this.selectedTrigger = this.triggerDetailsByName[triggerKey.name] || null; + this.selectedTrigger = this.triggerDetailsByName[this.getTriggerDetailKey(triggerKey)] || null; this.subscribeToTriggerTopics(this.selectedTriggerKey); - const subscription = this.schedulerService.getSimpleTriggerConfig(triggerKey.name).subscribe({ + const subscription = this.schedulerService.getSimpleTriggerConfig(triggerKey.name, this.getTriggerGroup(triggerKey)).subscribe({ next: trigger => { this.selectedTrigger = trigger as SimpleTrigger; - this.triggerDetailsByName[triggerKey.name] = trigger as SimpleTrigger; + this.triggerDetailsByName[this.getTriggerDetailKey(triggerKey)] = trigger as SimpleTrigger; this.triggerLoading = false; }, error: () => { @@ -285,6 +300,88 @@ export class ManagerComponent implements OnInit, OnDestroy { this.openDetailDrawer(); } + selectScheduledJob(job: ScheduledJob) { + this.selectedScheduledJob = job; + this.selectedJobClass = job?.jobClassName || this.selectedJobClass; + this.openDetailDrawer(); + } + + triggerSelectedJobNow() { + if (!this.selectedScheduledJob) { + this.showRoadmapNotice('Trigger-now requires a scheduled job key returned by the backend'); + return; + } + const subscription = this.jobService.triggerJob(this.selectedScheduledJob).subscribe({ + next: () => this.operationNotice = 'Job triggered.', + error: () => this.operationError = 'Unable to trigger the selected job.' + }); + this.subscriptions.push(subscription); + } + + deleteSelectedJob() { + if (!this.selectedScheduledJob) { + this.showRoadmapNotice('Delete requires a scheduled job key returned by the backend'); + return; + } + if (!window.confirm(`Delete job ${this.selectedScheduledJob.jobKeyDTO.group}.${this.selectedScheduledJob.jobKeyDTO.name}?`)) { + return; + } + const subscription = this.jobService.deleteJob(this.selectedScheduledJob).subscribe({ + next: () => { + this.operationNotice = 'Job deleted.'; + this.scheduledJobs = this.scheduledJobs.filter(job => !this.sameJob(job, this.selectedScheduledJob)); + this.selectedScheduledJob = this.scheduledJobs[0]; + }, + error: () => this.operationError = 'Unable to delete the selected job.' + }); + this.subscriptions.push(subscription); + } + + pauseSelectedTrigger() { + if (!this.selectedTriggerKey) { + return; + } + const subscription = this.triggerService.pauseTrigger(this.selectedTriggerKey).subscribe({ + next: () => this.setSelectedTriggerState('PAUSED', 'Trigger paused.'), + error: () => this.operationError = 'Unable to pause the selected trigger.' + }); + this.subscriptions.push(subscription); + } + + resumeSelectedTrigger() { + if (!this.selectedTriggerKey) { + return; + } + const subscription = this.triggerService.resumeTrigger(this.selectedTriggerKey).subscribe({ + next: () => this.setSelectedTriggerState('NORMAL', 'Trigger resumed.'), + error: () => this.operationError = 'Unable to resume the selected trigger.' + }); + this.subscriptions.push(subscription); + } + + unscheduleSelectedTrigger() { + if (!this.selectedTriggerKey) { + return; + } + if (!window.confirm(`Unschedule trigger ${this.getSelectedTriggerGroup()}.${this.selectedTriggerKey.name}?`)) { + return; + } + const triggerKey = {...this.selectedTriggerKey}; + const subscription = this.triggerService.unscheduleTrigger(triggerKey).subscribe({ + next: () => { + this.operationNotice = 'Trigger unscheduled.'; + this.triggerKeys = this.triggerKeys.filter(currentTriggerKey => !this.sameTriggerKey(currentTriggerKey, triggerKey)); + delete this.triggerDetailsByName[this.getTriggerDetailKey(triggerKey)]; + this.selectedTriggerKey = this.triggerKeys[0]; + this.selectedTrigger = this.selectedTriggerKey + ? this.triggerDetailsByName[this.getTriggerDetailKey(this.selectedTriggerKey)] + : null; + }, + error: () => this.operationError = 'Unable to unschedule the selected trigger.' + }); + this.subscriptions.push(subscription); + } + openCreateTriggerWizard() { this.resetWizard(); this.wizardOpen = true; @@ -302,7 +399,7 @@ export class ManagerComponent implements OnInit, OnDestroy { return; } - const trigger = this.selectedTrigger || this.triggerDetailsByName[this.selectedTriggerKey.name]; + const trigger = this.selectedTrigger || this.triggerDetailsByName[this.getTriggerDetailKey(this.selectedTriggerKey)]; const repeatInterval = this.splitRepeatInterval(trigger?.repeatInterval || 60000); this.wizardMode = 'edit'; this.wizardOpen = true; @@ -337,6 +434,7 @@ export class ManagerComponent implements OnInit, OnDestroy { const command = new SimpleTriggerCommand(); command.triggerName = this.triggerDraft.triggerName.trim(); + command.triggerGroup = this.triggerDraft.group || 'DEFAULT'; command.jobClass = this.triggerDraft.jobClass; command.startDate = this.fromDatetimeLocalValue(this.triggerDraft.startDate); command.endDate = this.fromDatetimeLocalValue(this.triggerDraft.endDate); @@ -352,7 +450,7 @@ export class ManagerComponent implements OnInit, OnDestroy { const subscription = request.subscribe({ next: trigger => { this.wizardSubmitting = false; - this.triggerDetailsByName[trigger.triggerKeyDTO.name] = trigger as SimpleTrigger; + this.triggerDetailsByName[this.getTriggerDetailKey(trigger.triggerKeyDTO)] = trigger as SimpleTrigger; this.upsertTriggerKey(trigger.triggerKeyDTO); this.selectTrigger(trigger.triggerKeyDTO); this.wizardOpen = false; @@ -380,7 +478,7 @@ export class ManagerComponent implements OnInit, OnDestroy { } getTriggerDetail(triggerKey: TriggerKey): SimpleTrigger { - return triggerKey?.name ? this.triggerDetailsByName[triggerKey.name] : null; + return triggerKey?.name ? this.triggerDetailsByName[this.getTriggerDetailKey(triggerKey)] : null; } getTriggerGroup(triggerKey: TriggerKey): string { @@ -388,7 +486,7 @@ export class ManagerComponent implements OnInit, OnDestroy { } getTriggerType(triggerKey: TriggerKey): string { - return this.getTriggerDetail(triggerKey) ? 'SimpleTrigger' : 'SimpleTrigger'; + return this.getTriggerDetail(triggerKey)?.type || 'SimpleTrigger'; } getTriggerState(triggerKey: TriggerKey): string { @@ -396,6 +494,9 @@ export class ManagerComponent implements OnInit, OnDestroy { if (!trigger) { return 'UNKNOWN'; } + if (trigger.state) { + return trigger.state; + } if (!trigger.mayFireAgain) { return 'COMPLETE'; } @@ -421,7 +522,10 @@ export class ManagerComponent implements OnInit, OnDestroy { getTriggerJobName(triggerKey: TriggerKey): string { const trigger = this.getTriggerDetail(triggerKey); - return trigger?.jobKeyDTO?.name || this.shortClassName(trigger?.jobDetailDTO?.jobClassName) || 'Roadmap'; + const jobGroup = trigger?.jobKeyDTO?.group ? `${trigger.jobKeyDTO.group}.` : ''; + return trigger?.jobKeyDTO?.name + ? `${jobGroup}${trigger.jobKeyDTO.name}` + : this.shortClassName(trigger?.jobDetailDTO?.jobClassName) || 'Roadmap'; } getTriggerNextFireLabel(triggerKey: TriggerKey): string { @@ -481,7 +585,18 @@ export class ManagerComponent implements OnInit, OnDestroy { } getSelectedJobShortName(): string { - return this.shortClassName(this.selectedJobClass) || '-'; + return this.shortClassName(this.selectedScheduledJob?.jobClassName || this.selectedJobClass) || '-'; + } + + getSelectedJobKeyLabel(): string { + if (!this.selectedScheduledJob?.jobKeyDTO) { + return '-'; + } + return `${this.selectedScheduledJob.jobKeyDTO.group}.${this.selectedScheduledJob.jobKeyDTO.name}`; + } + + getScheduledJobRows(): ScheduledJob[] { + return this.scheduledJobs || []; } getWizardTitle(): string { @@ -561,6 +676,16 @@ export class ManagerComponent implements OnInit, OnDestroy { this.roadmapNotice = null; } + private setSelectedTriggerState(state: string, notice: string) { + if (this.selectedTrigger) { + this.selectedTrigger.state = state; + } + if (this.selectedTriggerKey?.name && this.triggerDetailsByName[this.getTriggerDetailKey(this.selectedTriggerKey)]) { + this.triggerDetailsByName[this.getTriggerDetailKey(this.selectedTriggerKey)].state = state; + } + this.operationNotice = notice; + } + private subscribeToTriggerTopics(triggerKey: TriggerKey) { this.unsubscribeFromTriggerTopics(); this.logs = []; @@ -598,11 +723,23 @@ export class ManagerComponent implements OnInit, OnDestroy { } private upsertTriggerKey(triggerKey: TriggerKey) { - if (!this.triggerKeys.some(currentTriggerKey => currentTriggerKey.name === triggerKey.name)) { + if (!this.triggerKeys.some(currentTriggerKey => this.sameTriggerKey(currentTriggerKey, triggerKey))) { this.triggerKeys = [triggerKey, ...this.triggerKeys]; } } + private sameTriggerKey(first: TriggerKey, second: TriggerKey): boolean { + return first?.name === second?.name && this.getTriggerGroup(first) === this.getTriggerGroup(second); + } + + private getTriggerDetailKey(triggerKey: TriggerKey): string { + return `${this.getTriggerGroup(triggerKey)}.${triggerKey.name}`; + } + + private sameJob(first: ScheduledJob, second: ScheduledJob): boolean { + return first?.jobKeyDTO?.name === second?.jobKeyDTO?.name && first?.jobKeyDTO?.group === second?.jobKeyDTO?.group; + } + private buildEmptyDraft(): TriggerDraft { return { triggerName: '', diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/JobController.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/JobController.java index ad47df2..9b198e0 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/JobController.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/JobController.java @@ -8,26 +8,35 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import it.fabioformosa.quartzmanager.api.common.config.OpenAPIConfigConsts; import it.fabioformosa.quartzmanager.api.common.config.QuartzManagerPaths; +import it.fabioformosa.quartzmanager.api.dto.ScheduledJobDTO; +import it.fabioformosa.quartzmanager.api.exceptions.JobNotFoundException; import it.fabioformosa.quartzmanager.api.services.JobService; +import org.quartz.SchedulerException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.stream.Collectors; -@RequestMapping(JobController.JOB_CONTROLLER_BASE_URL) +@RequestMapping(QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH) @SecurityRequirement(name = OpenAPIConfigConsts.QUARTZ_MANAGER_SEC_OAS_SCHEMA) @RestController public class JobController { public static final String JOB_CONTROLLER_BASE_URL = QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/jobs"; + public static final String JOB_CLASSES_CONTROLLER_BASE_URL = QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/job-classes"; private final JobService jobService; public JobController(JobService jobService) { this.jobService = jobService; } - @GetMapping + @GetMapping("/job-classes") @Operation(summary = "Get the list of job classes eligible for Quartz-Manager") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Return a list of qualified java classes", @@ -38,4 +47,35 @@ public class JobController { return jobService.getJobClasses().stream().map(Class::getName).collect(Collectors.toList()); } + @GetMapping("/jobs") + @Operation(summary = "Get the list of scheduled jobs") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Return a list of scheduled jobs", + content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = ScheduledJobDTO.class))}) + }) + public List listScheduledJobs() throws SchedulerException { + return jobService.fetchScheduledJobs(); + } + + @GetMapping("/jobs/{group}/{name}") + @Operation(summary = "Get a scheduled job") + public ScheduledJobDTO getScheduledJob(@PathVariable String group, @PathVariable String name) throws SchedulerException, JobNotFoundException { + return jobService.getScheduledJob(group, name); + } + + @PostMapping("/jobs/{group}/{name}/trigger") + @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Trigger a job now") + public void triggerJob(@PathVariable String group, @PathVariable String name) throws SchedulerException, JobNotFoundException { + jobService.triggerJob(group, name); + } + + @DeleteMapping("/jobs/{group}/{name}") + @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Delete a job") + public void deleteJob(@PathVariable String group, @PathVariable String name) throws SchedulerException, JobNotFoundException { + jobService.deleteJob(group, name); + } + } diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/SchedulerController.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/SchedulerController.java index bce342c..1bcfef1 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/SchedulerController.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/SchedulerController.java @@ -12,6 +12,7 @@ import lombok.extern.slf4j.Slf4j; import org.quartz.SchedulerException; 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.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @@ -51,21 +52,21 @@ public class SchedulerController { return schedulerService.getScheduler(); } - @GetMapping("/pause") - @Operation(summary = "Get paused the scheduler") + @PostMapping("/standby") + @Operation(summary = "Put the scheduler in standby mode") @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Got paused successfully") + @ApiResponse(responseCode = "204", description = "Scheduler moved to standby successfully") }) @ResponseStatus(HttpStatus.NO_CONTENT) - public void pause() throws SchedulerException { - log.info("SCHEDULER - PAUSE COMMAND"); + public void standby() throws SchedulerException { + log.info("SCHEDULER - STANDBY COMMAND"); schedulerService.standby(); } - @GetMapping("/resume") - @Operation(summary = "Get resumed the scheduler") + @PostMapping("/resume") + @Operation(summary = "Resume the scheduler from standby mode") @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Got resumed successfully") + @ApiResponse(responseCode = "204", description = "Scheduler resumed successfully") }) @ResponseStatus(HttpStatus.NO_CONTENT) public void resume() throws SchedulerException { @@ -73,25 +74,25 @@ public class SchedulerController { schedulerService.start(); } - @GetMapping("/run") + @PostMapping("/start") @Operation(summary = "Start the scheduler") @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Got started successfully") + @ApiResponse(responseCode = "204", description = "Scheduler started successfully") }) @ResponseStatus(HttpStatus.NO_CONTENT) - public void run() throws SchedulerException { + public void start() throws SchedulerException { log.info("SCHEDULER - START COMMAND"); schedulerService.start(); } - @GetMapping("/stop") - @Operation(summary = "Stop the scheduler") + @PostMapping("/shutdown") + @Operation(summary = "Shutdown the scheduler terminally") @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Got stopped successfully") + @ApiResponse(responseCode = "204", description = "Scheduler shut down successfully") }) @ResponseStatus(HttpStatus.NO_CONTENT) - public void stop() throws SchedulerException { - log.info("SCHEDULER - STOP COMMAND"); + public void shutdown() throws SchedulerException { + log.info("SCHEDULER - SHUTDOWN COMMAND"); schedulerService.shutdown(); } diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/SimpleTriggerController.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/SimpleTriggerController.java index 1fd1b4c..0d45bbf 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/SimpleTriggerController.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/SimpleTriggerController.java @@ -35,7 +35,7 @@ public class SimpleTriggerController { this.simpleSchedulerService = simpleSchedulerService; } - @GetMapping("/{name}") + @GetMapping("/{group}/{name}") @Operation(summary = "Get a simple trigger by name") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Got the trigger by its name", @@ -44,11 +44,11 @@ public class SimpleTriggerController { @ApiResponse(responseCode = "404", description = "Trigger not found", content = @Content) }) - public SimpleTriggerDTO getSimpleTrigger(@PathVariable String name) throws SchedulerException, TriggerNotFoundException { - return simpleSchedulerService.getSimpleTriggerByName(name); + public SimpleTriggerDTO getSimpleTrigger(@PathVariable String group, @PathVariable String name) throws SchedulerException, TriggerNotFoundException { + return simpleSchedulerService.getSimpleTrigger(group, name); } - @PostMapping("/{name}") + @PostMapping("/{group}/{name}") @ResponseStatus(HttpStatus.CREATED) @Operation(summary = "Schedule a new simple trigger") @ApiResponses(value = { @@ -58,10 +58,11 @@ public class SimpleTriggerController { @ApiResponse(responseCode = "400", description = "Invalid trigger configuration", content = @Content) }) - public SimpleTriggerDTO postSimpleTrigger(@PathVariable String name, @Valid @RequestBody SimpleTriggerInputDTO simpleTriggerInputDTO) throws SchedulerException, ClassNotFoundException { + public SimpleTriggerDTO postSimpleTrigger(@PathVariable String group, @PathVariable String name, @Valid @RequestBody SimpleTriggerInputDTO simpleTriggerInputDTO) throws SchedulerException, ClassNotFoundException { log.info("SIMPLE TRIGGER - CREATING a SimpleTrigger {} {}", name, simpleTriggerInputDTO); SimpleTriggerCommandDTO simpleTriggerCommandDTO = SimpleTriggerCommandDTO.builder() .triggerName(name) + .triggerGroup(group) .simpleTriggerInputDTO(simpleTriggerInputDTO) .build(); SimpleTriggerDTO newTriggerDTO = simpleSchedulerService.scheduleSimpleTrigger(simpleTriggerCommandDTO); @@ -69,7 +70,7 @@ public class SimpleTriggerController { return newTriggerDTO; } - @PutMapping("/{name}") + @PutMapping("/{group}/{name}") @Operation(summary = "Reschedule a simple trigger") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Rescheduled a simple trigger", @@ -78,10 +79,11 @@ public class SimpleTriggerController { @ApiResponse(responseCode = "400", description = "Invalid trigger configuration", content = @Content) }) - public TriggerDTO rescheduleSimpleTrigger(@PathVariable String name, @Valid @RequestBody SimpleTriggerInputDTO simpleTriggerInputDTO) throws SchedulerException { + public TriggerDTO rescheduleSimpleTrigger(@PathVariable String group, @PathVariable String name, @Valid @RequestBody SimpleTriggerInputDTO simpleTriggerInputDTO) throws SchedulerException, TriggerNotFoundException { log.info("SIMPLE TRIGGER - RESCHEDULING the trigger {} {}", name, simpleTriggerInputDTO); SimpleTriggerCommandDTO simpleTriggerCommandDTO = SimpleTriggerCommandDTO.builder() .triggerName(name) + .triggerGroup(group) .simpleTriggerInputDTO(simpleTriggerInputDTO) .build(); TriggerDTO triggerDTO = simpleSchedulerService.rescheduleSimpleTrigger(simpleTriggerCommandDTO); diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/TriggerController.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/TriggerController.java index 72726ee..2e42602 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/TriggerController.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/TriggerController.java @@ -7,11 +7,18 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import it.fabioformosa.quartzmanager.api.dto.TriggerKeyDTO; +import it.fabioformosa.quartzmanager.api.dto.TriggerDTO; +import it.fabioformosa.quartzmanager.api.exceptions.TriggerNotFoundException; import it.fabioformosa.quartzmanager.api.services.TriggerService; import lombok.extern.slf4j.Slf4j; import org.quartz.SchedulerException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -44,4 +51,37 @@ public class TriggerController { return triggerService.fetchTriggers(); } + @GetMapping("/{group}/{name}") + @Operation(summary = "Get trigger details") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Got trigger details", + content = { @Content(mediaType = "application/json", + schema = @Schema(implementation = TriggerDTO.class)) }), + @ApiResponse(responseCode = "404", description = "Trigger not found", content = @Content) + }) + public TriggerDTO getTrigger(@PathVariable String group, @PathVariable String name) throws SchedulerException, TriggerNotFoundException { + return triggerService.getTrigger(group, name); + } + + @PostMapping("/{group}/{name}/pause") + @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Pause a trigger") + public void pauseTrigger(@PathVariable String group, @PathVariable String name) throws SchedulerException, TriggerNotFoundException { + triggerService.pauseTrigger(group, name); + } + + @PostMapping("/{group}/{name}/resume") + @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Resume a trigger") + public void resumeTrigger(@PathVariable String group, @PathVariable String name) throws SchedulerException, TriggerNotFoundException { + triggerService.resumeTrigger(group, name); + } + + @DeleteMapping("/{group}/{name}") + @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Unschedule a trigger") + public void unscheduleTrigger(@PathVariable String group, @PathVariable String name) throws SchedulerException, TriggerNotFoundException { + triggerService.unscheduleTrigger(group, name); + } + } diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/advices/ExceptionHandlingController.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/advices/ExceptionHandlingController.java index d59acb5..579d128 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/advices/ExceptionHandlingController.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/controllers/advices/ExceptionHandlingController.java @@ -1,8 +1,10 @@ package it.fabioformosa.quartzmanager.api.controllers.advices; import it.fabioformosa.quartzmanager.api.exceptions.ExceptionResponse; +import it.fabioformosa.quartzmanager.api.exceptions.JobNotFoundException; import it.fabioformosa.quartzmanager.api.exceptions.ResourceConflictException; import it.fabioformosa.quartzmanager.api.exceptions.TriggerNotFoundException; +import it.fabioformosa.quartzmanager.api.exceptions.UnsupportedTriggerTypeException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -28,4 +30,20 @@ public class ExceptionHandlingController { return ExceptionResponse.builder().errorCode(HttpStatus.NOT_FOUND.toString()).errorMessage(ex.getMessage()).build(); } + @ExceptionHandler(JobNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + @ResponseBody + public ExceptionResponse jobNotFound(JobNotFoundException ex){ + return ExceptionResponse.builder().errorCode(HttpStatus.NOT_FOUND.toString()).errorMessage(ex.getMessage()).build(); + } + + @ExceptionHandler(UnsupportedTriggerTypeException.class) + public ResponseEntity unsupportedTriggerType(UnsupportedTriggerTypeException ex) { + ExceptionResponse response = ExceptionResponse.builder() + .errorCode(HttpStatus.CONFLICT.toString()) + .errorMessage(ex.getMessage()) + .build(); + return new ResponseEntity<>(response, HttpStatus.CONFLICT); + } + } diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/converters/SchedulerToSchedulerDTO.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/converters/SchedulerToSchedulerDTO.java index 1a0ca26..01978ee 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/converters/SchedulerToSchedulerDTO.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/converters/SchedulerToSchedulerDTO.java @@ -6,9 +6,13 @@ import it.fabioformosa.quartzmanager.api.enums.SchedulerStatus; import lombok.SneakyThrows; import org.quartz.Scheduler; import org.quartz.SchedulerException; +import org.quartz.SchedulerMetaData; import org.quartz.impl.matchers.GroupMatcher; import org.springframework.stereotype.Component; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + @Component public class SchedulerToSchedulerDTO extends AbstractBaseConverterToDTO { @@ -20,6 +24,16 @@ public class SchedulerToSchedulerDTO extends AbstractBaseConverterToDTO extend target.setFinalFireTime(source.getFinalFireTime()); target.setMisfireInstruction(source.getMisfireInstruction()); target.setNextFireTime(source.getNextFireTime()); + target.setPreviousFireTime(source.getPreviousFireTime()); + target.setCalendarName(source.getCalendarName()); + target.setType(source.getClass().getSimpleName()); target.setPriority(source.getPriority()); target.setMayFireAgain(source.mayFireAgain()); diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/dto/ScheduledJobDTO.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/dto/ScheduledJobDTO.java new file mode 100644 index 0000000..039a7fc --- /dev/null +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/dto/ScheduledJobDTO.java @@ -0,0 +1,23 @@ +package it.fabioformosa.quartzmanager.api.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Data +public class ScheduledJobDTO { + private JobKeyDTO jobKeyDTO; + private String jobClassName; + private String description; + private boolean durable; + private boolean requestsRecovery; + private Map jobDataMap; + private List triggerKeys; +} diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/dto/SchedulerDTO.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/dto/SchedulerDTO.java index 07dfd21..71cc0a5 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/dto/SchedulerDTO.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/dto/SchedulerDTO.java @@ -18,4 +18,12 @@ public class SchedulerDTO { private String instanceId; private SchedulerStatus status; private Set triggerKeys; + private String quartzVersion; + private String jobStoreClass; + private boolean jobStoreSupportsPersistence; + private boolean clustered; + private String threadPoolClass; + private int threadPoolSize; + private String runningSince; + private int numberOfJobsExecuted; } diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/dto/SimpleTriggerCommandDTO.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/dto/SimpleTriggerCommandDTO.java index ae8c622..0e4cf79 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/dto/SimpleTriggerCommandDTO.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/dto/SimpleTriggerCommandDTO.java @@ -9,5 +9,6 @@ import lombok.*; @ToString public class SimpleTriggerCommandDTO { private String triggerName; + private String triggerGroup; private SimpleTriggerInputDTO simpleTriggerInputDTO; } diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/dto/TriggerDTO.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/dto/TriggerDTO.java index 0e1d772..eb645c5 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/dto/TriggerDTO.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/dto/TriggerDTO.java @@ -21,6 +21,10 @@ public class TriggerDTO { private Date finalFireTime; private int misfireInstruction; private Date nextFireTime; + private Date previousFireTime; + private String type; + private String state; + private String calendarName; private JobKeyDTO jobKeyDTO; private JobDetailDTO jobDetailDTO; private boolean mayFireAgain; diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/exceptions/JobNotFoundException.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/exceptions/JobNotFoundException.java new file mode 100644 index 0000000..9c6c7b3 --- /dev/null +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/exceptions/JobNotFoundException.java @@ -0,0 +1,7 @@ +package it.fabioformosa.quartzmanager.api.exceptions; + +public class JobNotFoundException extends Exception { + public JobNotFoundException(String group, String name) { + super("Job " + group + "." + name + " not found!"); + } +} diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/exceptions/ResourceConflictException.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/exceptions/ResourceConflictException.java index c7ba754..02d728f 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/exceptions/ResourceConflictException.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/exceptions/ResourceConflictException.java @@ -12,4 +12,8 @@ public class ResourceConflictException extends RuntimeException { super("Conflict on resourceID " + resourceId + " " + message); } + public ResourceConflictException(String message) { + super(message); + } + } diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/exceptions/TriggerNotFoundException.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/exceptions/TriggerNotFoundException.java index d3e0113..d623e9f 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/exceptions/TriggerNotFoundException.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/exceptions/TriggerNotFoundException.java @@ -4,4 +4,8 @@ public class TriggerNotFoundException extends Exception { public TriggerNotFoundException(String name) { super("Trigger with name " + name + " not found!"); } + + public TriggerNotFoundException(String group, String name) { + super("Trigger " + group + "." + name + " not found!"); + } } diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/exceptions/UnsupportedTriggerTypeException.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/exceptions/UnsupportedTriggerTypeException.java new file mode 100644 index 0000000..611d169 --- /dev/null +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/exceptions/UnsupportedTriggerTypeException.java @@ -0,0 +1,7 @@ +package it.fabioformosa.quartzmanager.api.exceptions; + +public class UnsupportedTriggerTypeException extends RuntimeException { + public UnsupportedTriggerTypeException(String group, String name) { + super("Trigger " + group + "." + name + " is not a SimpleTrigger"); + } +} diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/services/JobService.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/services/JobService.java index 3433e4e..3a554a2 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/services/JobService.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/services/JobService.java @@ -1,11 +1,24 @@ package it.fabioformosa.quartzmanager.api.services; +import it.fabioformosa.quartzmanager.api.dto.JobKeyDTO; +import it.fabioformosa.quartzmanager.api.dto.ScheduledJobDTO; +import it.fabioformosa.quartzmanager.api.dto.TriggerKeyDTO; +import it.fabioformosa.quartzmanager.api.exceptions.JobNotFoundException; import it.fabioformosa.quartzmanager.api.jobs.AbstractQuartzManagerJob; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.Trigger; +import org.quartz.impl.matchers.GroupMatcher; import org.reflections.Reflections; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.convert.ConversionService; import org.springframework.stereotype.Service; import jakarta.annotation.PostConstruct; @@ -20,8 +33,19 @@ public class JobService { private List> jobClasses = new ArrayList<>(); private List jobClassPackages = new ArrayList<>(); + private final Scheduler scheduler; + private final ConversionService conversionService; - public JobService(@Value("${quartz-manager.jobClassPackages}") String jobClassPackages) { + public JobService(String jobClassPackages) { + this(jobClassPackages, null, null); + } + + @Autowired + public JobService(@Value("${quartz-manager.jobClassPackages}") String jobClassPackages, + @Qualifier("quartzManagerScheduler") Scheduler scheduler, + ConversionService conversionService) { + this.scheduler = scheduler; + this.conversionService = conversionService; List splitPackages = Arrays.stream(Optional.of(jobClassPackages).map(str -> str.split(",")) .orElseThrow(() -> new RuntimeException("The prop quartz-manager.jobClassPackages cannot be blank!"))) .map(String::trim) @@ -47,4 +71,54 @@ public class JobService { return reflections.getSubTypesOf(AbstractQuartzManagerJob.class); } + public List fetchScheduledJobs() throws SchedulerException { + Set jobKeys = scheduler.getJobKeys(GroupMatcher.anyJobGroup()); + return jobKeys.stream().map(this::convertJob).toList(); + } + + public ScheduledJobDTO getScheduledJob(String group, String name) throws SchedulerException, JobNotFoundException { + JobKey jobKey = JobKey.jobKey(name, group); + if (!scheduler.checkExists(jobKey)) + throw new JobNotFoundException(group, name); + return convertJob(jobKey); + } + + public void triggerJob(String group, String name) throws SchedulerException, JobNotFoundException { + JobKey jobKey = requireJob(group, name); + scheduler.triggerJob(jobKey); + } + + public void deleteJob(String group, String name) throws SchedulerException, JobNotFoundException { + JobKey jobKey = requireJob(group, name); + scheduler.deleteJob(jobKey); + } + + private JobKey requireJob(String group, String name) throws SchedulerException, JobNotFoundException { + JobKey jobKey = JobKey.jobKey(name, group); + if (!scheduler.checkExists(jobKey)) + throw new JobNotFoundException(group, name); + return jobKey; + } + + private ScheduledJobDTO convertJob(JobKey jobKey) { + try { + JobDetail jobDetail = scheduler.getJobDetail(jobKey); + List triggerKeys = scheduler.getTriggersOfJob(jobKey).stream() + .map(Trigger::getKey) + .map(triggerKey -> conversionService.convert(triggerKey, TriggerKeyDTO.class)) + .toList(); + return ScheduledJobDTO.builder() + .jobKeyDTO(conversionService.convert(jobKey, JobKeyDTO.class)) + .jobClassName(jobDetail.getJobClass().getName()) + .description(jobDetail.getDescription()) + .durable(jobDetail.isDurable()) + .requestsRecovery(jobDetail.requestsRecovery()) + .jobDataMap(jobDetail.getJobDataMap()) + .triggerKeys(triggerKeys) + .build(); + } catch (SchedulerException ex) { + throw new IllegalStateException("Unable to read job " + jobKey, ex); + } + } + } diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/services/SimpleTriggerService.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/services/SimpleTriggerService.java index d875966..ed783c3 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/services/SimpleTriggerService.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/services/SimpleTriggerService.java @@ -2,7 +2,9 @@ package it.fabioformosa.quartzmanager.api.services; import it.fabioformosa.quartzmanager.api.dto.SimpleTriggerCommandDTO; import it.fabioformosa.quartzmanager.api.dto.SimpleTriggerDTO; +import it.fabioformosa.quartzmanager.api.exceptions.ResourceConflictException; import it.fabioformosa.quartzmanager.api.exceptions.TriggerNotFoundException; +import it.fabioformosa.quartzmanager.api.exceptions.UnsupportedTriggerTypeException; import org.quartz.*; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.convert.ConversionService; @@ -16,11 +18,25 @@ public class SimpleTriggerService extends AbstractSchedulerService { } public SimpleTriggerDTO getSimpleTriggerByName(String name) throws SchedulerException, TriggerNotFoundException { - Trigger trigger = getTriggerByName(name); - return conversionService.convert(trigger, SimpleTriggerDTO.class); + return getSimpleTrigger("DEFAULT", name); + } + + public SimpleTriggerDTO getSimpleTrigger(String group, String name) throws SchedulerException, TriggerNotFoundException { + Trigger trigger = scheduler.getTrigger(TriggerKey.triggerKey(name, group)); + if (trigger == null) + throw new TriggerNotFoundException(group, name); + if (!(trigger instanceof SimpleTrigger simpleTrigger)) + throw new UnsupportedTriggerTypeException(group, name); + SimpleTriggerDTO simpleTriggerDTO = conversionService.convert(simpleTrigger, SimpleTriggerDTO.class); + simpleTriggerDTO.setState(scheduler.getTriggerState(simpleTrigger.getKey()).name()); + return simpleTriggerDTO; } public SimpleTriggerDTO scheduleSimpleTrigger(SimpleTriggerCommandDTO simpleTriggerCommandDTO) throws SchedulerException, ClassNotFoundException { + TriggerKey triggerKey = TriggerKey.triggerKey(simpleTriggerCommandDTO.getTriggerName(), simpleTriggerCommandDTO.getTriggerGroup()); + if (scheduler.checkExists(triggerKey)) + throw new ResourceConflictException("Trigger " + triggerKey + " already exists"); + Class jobClass = Class.forName(simpleTriggerCommandDTO.getSimpleTriggerInputDTO().getJobClass()).asSubclass(Job.class); JobDetail jobDetail = JobBuilder.newJob() .ofType(jobClass) @@ -33,10 +49,17 @@ public class SimpleTriggerService extends AbstractSchedulerService { return conversionService.convert(newSimpleTrigger, SimpleTriggerDTO.class); } - public SimpleTriggerDTO rescheduleSimpleTrigger(SimpleTriggerCommandDTO triggerCommandDTO) throws SchedulerException { - SimpleTrigger newSimpleTrigger = conversionService.convert(triggerCommandDTO, SimpleTrigger.class); + public SimpleTriggerDTO rescheduleSimpleTrigger(SimpleTriggerCommandDTO triggerCommandDTO) throws SchedulerException, TriggerNotFoundException { + TriggerKey triggerKey = TriggerKey.triggerKey(triggerCommandDTO.getTriggerName(), triggerCommandDTO.getTriggerGroup()); + Trigger existingTrigger = scheduler.getTrigger(triggerKey); + if (existingTrigger == null) + throw new TriggerNotFoundException(triggerCommandDTO.getTriggerGroup(), triggerCommandDTO.getTriggerName()); + + SimpleTrigger newSimpleTrigger = conversionService.convert(triggerCommandDTO, SimpleTrigger.class); + newSimpleTrigger = newSimpleTrigger.getTriggerBuilder() + .forJob(existingTrigger.getJobKey()) + .build(); - TriggerKey triggerKey = TriggerKey.triggerKey(triggerCommandDTO.getTriggerName()); scheduler.rescheduleJob(triggerKey, newSimpleTrigger); return conversionService.convert(newSimpleTrigger, SimpleTriggerDTO.class); diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/services/TriggerService.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/services/TriggerService.java index 9a1a71a..272ceed 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/services/TriggerService.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/services/TriggerService.java @@ -1,8 +1,11 @@ package it.fabioformosa.quartzmanager.api.services; +import it.fabioformosa.quartzmanager.api.dto.TriggerDTO; import it.fabioformosa.quartzmanager.api.dto.TriggerKeyDTO; +import it.fabioformosa.quartzmanager.api.exceptions.TriggerNotFoundException; import org.quartz.Scheduler; import org.quartz.SchedulerException; +import org.quartz.Trigger; import org.quartz.TriggerKey; import org.quartz.impl.matchers.GroupMatcher; import org.springframework.beans.factory.annotation.Qualifier; @@ -30,4 +33,36 @@ public class TriggerService { .toList(); } + public TriggerDTO getTrigger(String group, String name) throws SchedulerException, TriggerNotFoundException { + TriggerKey triggerKey = TriggerKey.triggerKey(name, group); + Trigger trigger = scheduler.getTrigger(triggerKey); + if (trigger == null) + throw new TriggerNotFoundException(group, name); + TriggerDTO triggerDTO = conversionService.convert(trigger, TriggerDTO.class); + triggerDTO.setState(scheduler.getTriggerState(triggerKey).name()); + return triggerDTO; + } + + public void pauseTrigger(String group, String name) throws SchedulerException, TriggerNotFoundException { + TriggerKey triggerKey = requireTrigger(group, name); + scheduler.pauseTrigger(triggerKey); + } + + public void resumeTrigger(String group, String name) throws SchedulerException, TriggerNotFoundException { + TriggerKey triggerKey = requireTrigger(group, name); + scheduler.resumeTrigger(triggerKey); + } + + public void unscheduleTrigger(String group, String name) throws SchedulerException, TriggerNotFoundException { + TriggerKey triggerKey = requireTrigger(group, name); + scheduler.unscheduleJob(triggerKey); + } + + private TriggerKey requireTrigger(String group, String name) throws SchedulerException, TriggerNotFoundException { + TriggerKey triggerKey = TriggerKey.triggerKey(name, group); + if (!scheduler.checkExists(triggerKey)) + throw new TriggerNotFoundException(group, name); + return triggerKey; + } + } diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/JobControllerTest.java b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/JobControllerTest.java index 9d7780d..74d94cf 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/JobControllerTest.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/JobControllerTest.java @@ -2,6 +2,8 @@ package it.fabioformosa.quartzmanager.api.controllers; import it.fabioformosa.quartzmanager.api.QuartManagerApplicationTests; import it.fabioformosa.quartzmanager.api.controllers.utils.TestUtils; +import it.fabioformosa.quartzmanager.api.dto.JobKeyDTO; +import it.fabioformosa.quartzmanager.api.dto.ScheduledJobDTO; import it.fabioformosa.quartzmanager.api.jobs.SampleJob; import it.fabioformosa.quartzmanager.api.services.JobService; import org.junit.jupiter.api.Test; @@ -16,11 +18,12 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import java.util.List; -import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @ContextConfiguration(classes = {QuartManagerApplicationTests.class}) -@WebMvcTest(controllers = SimpleTriggerController.class, properties = { +@WebMvcTest(controllers = JobController.class, properties = { "quartz-manager.jobClassPackages=it.fabioformosa.quartzmanager.jobs" }) class JobControllerTest { @@ -36,11 +39,44 @@ class JobControllerTest { Mockito.when(jobService.getJobClasses()).thenReturn(List.of(SampleJob.class)); List expectedJobs = List.of(SampleJob.class.getName()); - mockMvc.perform(get(JobController.JOB_CONTROLLER_BASE_URL) + mockMvc.perform(get(JobController.JOB_CLASSES_CONTROLLER_BASE_URL) .contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(expectedJobs))); Mockito.verify(jobService, Mockito.times(1)).getJobClasses(); } + @Test + void whenGetScheduledJobsIsCalled_thenScheduledJobsAreReturned() throws Exception { + ScheduledJobDTO scheduledJobDTO = ScheduledJobDTO.builder() + .jobKeyDTO(JobKeyDTO.builder().name("sampleJob").group("DEFAULT").build()) + .jobClassName(SampleJob.class.getName()) + .build(); + Mockito.when(jobService.fetchScheduledJobs()).thenReturn(List.of(scheduledJobDTO)); + + mockMvc.perform(get(JobController.JOB_CONTROLLER_BASE_URL) + .contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(List.of(scheduledJobDTO)))); + + Mockito.verify(jobService).fetchScheduledJobs(); + } + + @Test + void whenTriggerJobIsCalled_thenNoContentIsReturned() throws Exception { + mockMvc.perform(post(JobController.JOB_CONTROLLER_BASE_URL + "/DEFAULT/sampleJob/trigger") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isNoContent()); + + Mockito.verify(jobService).triggerJob("DEFAULT", "sampleJob"); + } + + @Test + void whenDeleteJobIsCalled_thenNoContentIsReturned() throws Exception { + mockMvc.perform(delete(JobController.JOB_CONTROLLER_BASE_URL + "/DEFAULT/sampleJob") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isNoContent()); + + Mockito.verify(jobService).deleteJob("DEFAULT", "sampleJob"); + } + } diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/SchedulerControllerTest.java b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/SchedulerControllerTest.java index fc59a54..1ac0a88 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/SchedulerControllerTest.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/SchedulerControllerTest.java @@ -16,9 +16,10 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @ContextConfiguration(classes = {QuartManagerApplicationTests.class}) -@WebMvcTest(controllers = SimpleTriggerController.class, properties = { +@WebMvcTest(controllers = SchedulerController.class, properties = { "quartz-manager.jobClassPackages=it.fabioformosa.quartzmanager.jobs" }) class SchedulerControllerTest { @@ -47,8 +48,8 @@ class SchedulerControllerTest { } @Test - void givenAScheduler_whenTheGetPausedIsCalled_then2xxReturned() throws Exception { - mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/pause") + void givenAScheduler_whenStandbyIsCalled_then2xxReturned() throws Exception { + mockMvc.perform(post(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/standby") .contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isNoContent()) .andExpect(MockMvcResultMatchers.content().string("")); @@ -57,8 +58,8 @@ class SchedulerControllerTest { } @Test - void givenAScheduler_whenTheGetResumedIsCalled_then2xxReturned() throws Exception { - mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/resume") + void givenAScheduler_whenResumeIsCalled_then2xxReturned() throws Exception { + mockMvc.perform(post(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/resume") .contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isNoContent()) .andExpect(MockMvcResultMatchers.content().string("")); @@ -67,8 +68,8 @@ class SchedulerControllerTest { } @Test - void givenAScheduler_whenTheGetRunIsCalled_then2xxReturned() throws Exception { - mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/run") + void givenAScheduler_whenStartIsCalled_then2xxReturned() throws Exception { + mockMvc.perform(post(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/start") .contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isNoContent()) .andExpect(MockMvcResultMatchers.content().string("")); @@ -77,8 +78,8 @@ class SchedulerControllerTest { } @Test - void givenAScheduler_whenTheGetStoppedIsCalled_then2xxReturned() throws Exception { - mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/stop") + void givenAScheduler_whenShutdownIsCalled_then2xxReturned() throws Exception { + mockMvc.perform(post(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/shutdown") .contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isNoContent()) .andExpect(MockMvcResultMatchers.content().string("")); diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/SimpleTriggerControllerTest.java b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/SimpleTriggerControllerTest.java index f4c396f..bd43c25 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/SimpleTriggerControllerTest.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/SimpleTriggerControllerTest.java @@ -46,9 +46,9 @@ class SimpleTriggerControllerTest { @Test void whenGetIsCalled_thenASimpleTriggerIsReturned() throws Exception { SimpleTriggerDTO expectedSimpleTriggerDTO = TriggerUtils.getSimpleTriggerInstance("mytrigger"); - Mockito.when(simpleTriggerService.getSimpleTriggerByName("mytrigger")).thenReturn(expectedSimpleTriggerDTO); + Mockito.when(simpleTriggerService.getSimpleTrigger("DEFAULT", "mytrigger")).thenReturn(expectedSimpleTriggerDTO); - mockMvc.perform(get(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/mytrigger") + mockMvc.perform(get(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/DEFAULT/mytrigger") .contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(expectedSimpleTriggerDTO))); } @@ -59,7 +59,7 @@ class SimpleTriggerControllerTest { SimpleTriggerDTO expectedSimpleTriggerDTO = TriggerUtils.getSimpleTriggerInstance("mytrigger", simpleTriggerInputDTO); Mockito.when(simpleTriggerService.scheduleSimpleTrigger(any())).thenReturn(expectedSimpleTriggerDTO); mockMvc.perform( - post(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/mytrigger") + post(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/DEFAULT/mytrigger") .contentType(MediaType.APPLICATION_JSON) .content(TestUtils.toJson(simpleTriggerInputDTO)) ) @@ -90,11 +90,12 @@ class SimpleTriggerControllerTest { SimpleTriggerDTO expectedSimpleTriggerDTO = TriggerUtils.getSimpleTriggerInstance("mytrigger", simpleTriggerInputDTO); SimpleTriggerCommandDTO simpleTriggerCommandDTO = SimpleTriggerCommandDTO.builder() .triggerName("mytrigger") + .triggerGroup("DEFAULT") .simpleTriggerInputDTO(simpleTriggerInputDTO) .build(); Mockito.when(simpleTriggerService.rescheduleSimpleTrigger(simpleTriggerCommandDTO)).thenReturn(expectedSimpleTriggerDTO); - mockMvc.perform(put(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/mytrigger") + mockMvc.perform(put(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/DEFAULT/mytrigger") .contentType(MediaType.APPLICATION_JSON) .content(TestUtils.toJson(simpleTriggerInputDTO))) .andExpect(MockMvcResultMatchers.status().isOk()) diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/SimpleTriggerControllerValidationTest.java b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/SimpleTriggerControllerValidationTest.java index 9cb795e..b33f3a7 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/SimpleTriggerControllerValidationTest.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/SimpleTriggerControllerValidationTest.java @@ -46,9 +46,9 @@ class SimpleTriggerControllerValidationTest { @Test void givenANotExistingTrigger_whenGetIsCalled_then404IsReturned() throws Exception { - Mockito.when(simpleTriggerService.getSimpleTriggerByName("not_existing_trigger_name")).thenThrow(new TriggerNotFoundException("not_existing_trigger_name")); + Mockito.when(simpleTriggerService.getSimpleTrigger("DEFAULT", "not_existing_trigger_name")).thenThrow(new TriggerNotFoundException("DEFAULT", "not_existing_trigger_name")); - mockMvc.perform(get(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/not_existing_trigger_name") + mockMvc.perform(get(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/DEFAULT/not_existing_trigger_name") .contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isNotFound()); } @@ -59,7 +59,7 @@ class SimpleTriggerControllerValidationTest { SimpleTriggerDTO expectedSimpleTriggerDTO = TriggerUtils.getSimpleTriggerInstance("my-minimal-trigger"); Mockito.when(simpleTriggerService.scheduleSimpleTrigger(any())).thenReturn(expectedSimpleTriggerDTO); mockMvc.perform( - post(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/my-minimal-trigger") + post(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/DEFAULT/my-minimal-trigger") .contentType(MediaType.APPLICATION_JSON) .content(TestUtils.toJson(simpleTriggerInputDTO)) ) @@ -83,7 +83,7 @@ class SimpleTriggerControllerValidationTest { SimpleTriggerDTO expectedSimpleTriggerDTO = TriggerUtils.getSimpleTriggerInstance("my-puntual-trigger"); Mockito.when(simpleTriggerService.scheduleSimpleTrigger(any())).thenReturn(expectedSimpleTriggerDTO); mockMvc.perform( - post(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/my-puntual-trigger") + post(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/DEFAULT/my-puntual-trigger") .contentType(MediaType.APPLICATION_JSON) .content(TestUtils.toJson(simpleTriggerInputDTO)) ) @@ -95,7 +95,7 @@ class SimpleTriggerControllerValidationTest { @ParameterizedTest @ArgumentsSource(InvalidSimpleTriggerCommandDTOProvider.class) void givenAnInvalidSimpleTriggerCommandDTO_whenPostedANewTrigger_thenAnErrorIsReturned(SimpleTriggerInputDTO invalidSimpleTriggerComandDTO) throws Exception { - mockMvc.perform(post(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/mytrigger") + mockMvc.perform(post(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/DEFAULT/mytrigger") .contentType(MediaType.APPLICATION_JSON) .content(TestUtils.toJson(invalidSimpleTriggerComandDTO))) .andExpect(MockMvcResultMatchers.status().is4xxClientError()); @@ -104,7 +104,7 @@ class SimpleTriggerControllerValidationTest { @ParameterizedTest @ArgumentsSource(InvalidSimpleTriggerCommandDTOProvider.class) void givenAnInvalidSimpleTriggerCommandDTO_whenATriggerIsRescheduled_thenAnErrorIsReturned(SimpleTriggerInputDTO invalidSimpleTriggerCommandTO) throws Exception { - mockMvc.perform(put(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/mytrigger") + mockMvc.perform(put(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/DEFAULT/mytrigger") .contentType(MediaType.APPLICATION_JSON) .content(TestUtils.toJson(invalidSimpleTriggerCommandTO))) .andExpect(MockMvcResultMatchers.status().is4xxClientError()); diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/TriggerControllerTest.java b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/TriggerControllerTest.java index 41322cc..27eec80 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/TriggerControllerTest.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/controllers/TriggerControllerTest.java @@ -1,14 +1,26 @@ package it.fabioformosa.quartzmanager.api.controllers; import it.fabioformosa.quartzmanager.api.QuartManagerApplicationTests; +import it.fabioformosa.quartzmanager.api.controllers.utils.TestUtils; +import it.fabioformosa.quartzmanager.api.dto.TriggerDTO; +import it.fabioformosa.quartzmanager.api.dto.TriggerKeyDTO; import it.fabioformosa.quartzmanager.api.services.TriggerService; 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.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.http.MediaType; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.List; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @ContextConfiguration(classes = {QuartManagerApplicationTests.class}) @WebMvcTest(controllers = TriggerController.class, properties = { @@ -27,4 +39,52 @@ class TriggerControllerTest { Mockito.reset(triggerService); } + @Test + void whenListTriggersIsCalled_thenTriggersAreReturned() throws Exception { + List triggerKeys = List.of(TriggerKeyDTO.builder().name("sampleTrigger").group("DEFAULT").build()); + Mockito.when(triggerService.fetchTriggers()).thenReturn(triggerKeys); + + mockMvc.perform(get(TriggerController.TRIGGER_CONTROLLER_BASE_URL).contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(triggerKeys))); + } + + @Test + void whenGetTriggerIsCalled_thenTriggerIsReturned() throws Exception { + TriggerDTO triggerDTO = TriggerDTO.builder() + .triggerKeyDTO(TriggerKeyDTO.builder().name("sampleTrigger").group("DEFAULT").build()) + .state("NORMAL") + .type("SimpleTrigger") + .build(); + Mockito.when(triggerService.getTrigger("DEFAULT", "sampleTrigger")).thenReturn(triggerDTO); + + mockMvc.perform(get(TriggerController.TRIGGER_CONTROLLER_BASE_URL + "/DEFAULT/sampleTrigger").contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(triggerDTO))); + } + + @Test + void whenPauseTriggerIsCalled_thenNoContentIsReturned() throws Exception { + mockMvc.perform(post(TriggerController.TRIGGER_CONTROLLER_BASE_URL + "/DEFAULT/sampleTrigger/pause").contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isNoContent()); + + Mockito.verify(triggerService).pauseTrigger("DEFAULT", "sampleTrigger"); + } + + @Test + void whenResumeTriggerIsCalled_thenNoContentIsReturned() throws Exception { + mockMvc.perform(post(TriggerController.TRIGGER_CONTROLLER_BASE_URL + "/DEFAULT/sampleTrigger/resume").contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isNoContent()); + + Mockito.verify(triggerService).resumeTrigger("DEFAULT", "sampleTrigger"); + } + + @Test + void whenUnscheduleTriggerIsCalled_thenNoContentIsReturned() throws Exception { + mockMvc.perform(delete(TriggerController.TRIGGER_CONTROLLER_BASE_URL + "/DEFAULT/sampleTrigger").contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isNoContent()); + + Mockito.verify(triggerService).unscheduleTrigger("DEFAULT", "sampleTrigger"); + } + } diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/services/SimpleTriggerServiceTest.java b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/services/SimpleTriggerServiceTest.java index e396c97..5b0d4ff 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/services/SimpleTriggerServiceTest.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/services/SimpleTriggerServiceTest.java @@ -47,7 +47,8 @@ class SimpleTriggerServiceTest { void givenAnExistingTrigger_whenGetSimplerTriggerByNameIsCalled_thenTheDtoIsReturned() throws SchedulerException, TriggerNotFoundException { String existing_trigger = "existing_trigger"; Mockito.when(scheduler.getTrigger(any(TriggerKey.class))) - .thenReturn(TriggerBuilder.newTrigger().withIdentity(existing_trigger).build()); + .thenReturn(TriggerBuilder.newTrigger().withIdentity(existing_trigger).withSchedule(SimpleScheduleBuilder.simpleSchedule()).build()); + Mockito.when(scheduler.getTriggerState(any(TriggerKey.class))).thenReturn(Trigger.TriggerState.NORMAL); Mockito.when(conversionService.convert(any(SimpleTrigger.class), eq(SimpleTriggerDTO.class))) .thenReturn(SimpleTriggerDTO.builder() .triggerKeyDTO(TriggerKeyDTO.builder().name(existing_trigger).build()) @@ -81,10 +82,14 @@ class SimpleTriggerServiceTest { .build(); Mockito.when(scheduler.scheduleJob(any(), any())).thenReturn(new Date()); + Mockito.when(scheduler.checkExists(any(TriggerKey.class))).thenReturn(false); + Mockito.when(conversionService.convert(any(SimpleTriggerCommandDTO.class), eq(SimpleTrigger.class))) + .thenReturn(TriggerBuilder.newTrigger().withIdentity(simpleTriggerName, "DEFAULT").withSchedule(SimpleScheduleBuilder.simpleSchedule()).build()); Mockito.when(conversionService.convert(any(), eq(SimpleTriggerDTO.class))).thenReturn(expectedTriggerDTO); SimpleTriggerCommandDTO simpleTriggerCommandDTO = SimpleTriggerCommandDTO.builder() .triggerName(simpleTriggerName) + .triggerGroup("DEFAULT") .simpleTriggerInputDTO(triggerInputDTO) .build(); SimpleTriggerDTO simpleTrigger = simpleSchedulerService.scheduleSimpleTrigger(simpleTriggerCommandDTO); @@ -93,7 +98,7 @@ class SimpleTriggerServiceTest { } @Test - void givenASimpleTriggerCommandDTO_whenASimpleTriggerIsRecheduled_thenATriggerDTOIsReturned() throws SchedulerException, ClassNotFoundException { + void givenASimpleTriggerCommandDTO_whenASimpleTriggerIsRecheduled_thenATriggerDTOIsReturned() throws SchedulerException, ClassNotFoundException, TriggerNotFoundException { SimpleTriggerInputDTO triggerInputDTO = SimpleTriggerInputDTO.builder() .jobClass("it.fabioformosa.quartzmanager.api.jobs.SampleJob") .startDate(new Date()) @@ -115,10 +120,15 @@ class SimpleTriggerServiceTest { .build(); Mockito.when(scheduler.rescheduleJob(any(), any())).thenReturn(new Date()); + Mockito.when(scheduler.getTrigger(any(TriggerKey.class))) + .thenReturn(TriggerBuilder.newTrigger().withIdentity(simpleTriggerName, "DEFAULT").forJob(JobKey.jobKey("MyJob", "DEFAULT")).withSchedule(SimpleScheduleBuilder.simpleSchedule()).build()); + Mockito.when(conversionService.convert(any(SimpleTriggerCommandDTO.class), eq(SimpleTrigger.class))) + .thenReturn(TriggerBuilder.newTrigger().withIdentity(simpleTriggerName, "DEFAULT").withSchedule(SimpleScheduleBuilder.simpleSchedule()).build()); Mockito.when(conversionService.convert(any(), eq(SimpleTriggerDTO.class))).thenReturn(expectedTriggerDTO); SimpleTriggerCommandDTO simpleTriggerCommandDTO = SimpleTriggerCommandDTO.builder() .triggerName(simpleTriggerName) + .triggerGroup("DEFAULT") .simpleTriggerInputDTO(triggerInputDTO) .build(); SimpleTriggerDTO simpleTrigger = simpleSchedulerService.rescheduleSimpleTrigger(simpleTriggerCommandDTO);