mirror of
https://github.com/fabioformosa/quartz-manager.git
synced 2026-05-14 22:00:30 +09:00
@@ -51,10 +51,11 @@ import {
|
||||
SchedulerService,
|
||||
ConfigService,
|
||||
getHtmlBaseUrl,
|
||||
LogsRxWebsocketService,
|
||||
ProgressRxWebsocketService,
|
||||
TriggerService
|
||||
} from './services';
|
||||
LogsRxWebsocketService,
|
||||
ProgressRxWebsocketService,
|
||||
TriggerService,
|
||||
CalendarService
|
||||
} from './services';
|
||||
import { ForbiddenComponent } from './views/forbidden/forbidden.component';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import JobService from './services/job.service';
|
||||
@@ -135,6 +136,7 @@ export function jwtOptionsFactory(apiService: ApiService) {
|
||||
SchedulerService,
|
||||
JobService,
|
||||
TriggerService,
|
||||
CalendarService,
|
||||
ProgressRxWebsocketService,
|
||||
LogsRxWebsocketService,
|
||||
AuthService,
|
||||
|
||||
24
quartz-manager-frontend/src/app/model/calendar.model.ts
Normal file
24
quartz-manager-frontend/src/app/model/calendar.model.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {TriggerKey} from './triggerKey.model';
|
||||
|
||||
export type CalendarType = 'ANNUAL' | 'CRON' | 'DAILY' | 'HOLIDAY' | 'MONTHLY' | 'WEEKLY';
|
||||
|
||||
export class QuartzCalendar {
|
||||
name: string;
|
||||
type: CalendarType = 'WEEKLY';
|
||||
description: string;
|
||||
cronExpression: string;
|
||||
timeZone: string;
|
||||
rangeStartingTime: string;
|
||||
rangeEndingTime: string;
|
||||
invertTimeRange: boolean;
|
||||
excludedDaysOfWeek: number[];
|
||||
excludedDaysOfMonth: number[];
|
||||
excludedDates: Date[];
|
||||
triggerKeys: TriggerKey[];
|
||||
}
|
||||
|
||||
export class CalendarIncludedTimeTest {
|
||||
time: Date;
|
||||
included: boolean;
|
||||
nextIncludedTime: Date;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {JobKeyModel} from './jobKey.model';
|
||||
|
||||
export type TriggerType = 'SIMPLE' | 'CRON' | 'DAILY_TIME_INTERVAL' | 'CALENDAR_INTERVAL';
|
||||
|
||||
export class TriggerCommand {
|
||||
triggerType: TriggerType = 'SIMPLE';
|
||||
jobClass: string;
|
||||
jobKey: JobKeyModel;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
description: string;
|
||||
priority: number;
|
||||
calendarName: string;
|
||||
misfireInstruction: string;
|
||||
jobDataMap: {[key: string]: unknown};
|
||||
repeatCount: number;
|
||||
repeatInterval: number;
|
||||
repeatIntervalUnit: string;
|
||||
cronExpression: string;
|
||||
timeZone: string;
|
||||
startTimeOfDay: string;
|
||||
endTimeOfDay: string;
|
||||
daysOfWeek: number[];
|
||||
preserveHourOfDayAcrossDaylightSavings: boolean;
|
||||
skipDayIfHourDoesNotExist: boolean;
|
||||
}
|
||||
@@ -19,4 +19,14 @@ export class Trigger {
|
||||
jobDetailDTO: JobDetail = new JobDetail();
|
||||
mayFireAgain: boolean;
|
||||
jobDataMap: {[key: string]: unknown};
|
||||
cronExpression: string;
|
||||
timeZone: string;
|
||||
repeatInterval: number;
|
||||
repeatCount: number;
|
||||
repeatIntervalUnit: string;
|
||||
startTimeOfDay: string;
|
||||
endTimeOfDay: string;
|
||||
daysOfWeek: number[];
|
||||
preserveHourOfDayAcrossDaylightSavings: boolean;
|
||||
skipDayIfHourDoesNotExist: boolean;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import {jest} from '@jest/globals';
|
||||
import {CalendarService} from './calendar.service';
|
||||
|
||||
describe('CalendarService', () => {
|
||||
let apiService: any;
|
||||
let calendarService: CalendarService;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = {
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
delete: jest.fn()
|
||||
};
|
||||
calendarService = new CalendarService(apiService);
|
||||
});
|
||||
|
||||
it('uses calendar registry endpoints', () => {
|
||||
const calendar: any = {name: 'weekends', type: 'WEEKLY'};
|
||||
const time = new Date('2026-05-12T12:00:00.000Z');
|
||||
|
||||
calendarService.fetchCalendars();
|
||||
calendarService.getCalendar('weekends');
|
||||
calendarService.createCalendar('weekends', calendar);
|
||||
calendarService.updateCalendar('weekends', calendar);
|
||||
calendarService.deleteCalendar('weekends');
|
||||
calendarService.testIncludedTime('weekends', time);
|
||||
|
||||
expect(apiService.get).toHaveBeenCalledWith('/quartz-manager/calendars');
|
||||
expect(apiService.get).toHaveBeenCalledWith('/quartz-manager/calendars/weekends');
|
||||
expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/calendars/weekends', calendar);
|
||||
expect(apiService.put).toHaveBeenCalledWith('/quartz-manager/calendars/weekends', calendar);
|
||||
expect(apiService.delete).toHaveBeenCalledWith('/quartz-manager/calendars/weekends');
|
||||
expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/calendars/weekends/included-time-test', {time});
|
||||
});
|
||||
});
|
||||
34
quartz-manager-frontend/src/app/services/calendar.service.ts
Normal file
34
quartz-manager-frontend/src/app/services/calendar.service.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Observable} from 'rxjs';
|
||||
import {ApiService} from './api.service';
|
||||
import {CONTEXT_PATH, getBaseUrl} from './config.service';
|
||||
import {CalendarIncludedTimeTest, QuartzCalendar} from '../model/calendar.model';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
fetchCalendars = (): Observable<QuartzCalendar[]> => {
|
||||
return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/calendars`);
|
||||
}
|
||||
|
||||
getCalendar = (name: string): Observable<QuartzCalendar> => {
|
||||
return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/calendars/${name}`);
|
||||
}
|
||||
|
||||
createCalendar = (name: string, calendar: QuartzCalendar): Observable<QuartzCalendar> => {
|
||||
return this.apiService.post(getBaseUrl() + `${CONTEXT_PATH}/calendars/${name}`, calendar);
|
||||
}
|
||||
|
||||
updateCalendar = (name: string, calendar: QuartzCalendar): Observable<QuartzCalendar> => {
|
||||
return this.apiService.put(getBaseUrl() + `${CONTEXT_PATH}/calendars/${name}`, calendar);
|
||||
}
|
||||
|
||||
deleteCalendar = (name: string): Observable<void> => {
|
||||
return this.apiService.delete(getBaseUrl() + `${CONTEXT_PATH}/calendars/${name}`);
|
||||
}
|
||||
|
||||
testIncludedTime = (name: string, time: Date): Observable<CalendarIncludedTimeTest> => {
|
||||
return this.apiService.post(getBaseUrl() + `${CONTEXT_PATH}/calendars/${name}/included-time-test`, {time});
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@ export * from './auth.service';
|
||||
export * from './scheduler.service';
|
||||
export * from './progress.rx-websocket.service';
|
||||
export * from './logs.rx-websocket.service';
|
||||
export * from './trigger.service'
|
||||
export * from './job.service'
|
||||
export * from './trigger.service'
|
||||
export * from './calendar.service'
|
||||
export * from './job.service'
|
||||
|
||||
|
||||
|
||||
@@ -18,21 +18,28 @@ describe('JobService', () => {
|
||||
|
||||
it('uses job class and scheduled job endpoints', () => {
|
||||
const job = new ScheduledJob();
|
||||
const command = {
|
||||
jobClass: 'SampleJob',
|
||||
description: '',
|
||||
durable: true,
|
||||
requestsRecovery: false,
|
||||
jobDataMap: {}
|
||||
};
|
||||
job.jobKeyDTO = {group: 'DEFAULT', name: 'sampleJob'};
|
||||
|
||||
jobService.fetchJobs();
|
||||
jobService.fetchScheduledJobs();
|
||||
jobService.getScheduledJob('DEFAULT', 'sampleJob');
|
||||
jobService.createJob('DEFAULT', 'sampleJob', {jobClass: 'SampleJob', description: '', durable: true, requestsRecovery: false, jobDataMap: {}});
|
||||
jobService.updateJob('DEFAULT', 'sampleJob', {jobClass: 'SampleJob', description: '', durable: true, requestsRecovery: false, jobDataMap: {}});
|
||||
jobService.createJob('DEFAULT', 'sampleJob', command);
|
||||
jobService.updateJob('DEFAULT', 'sampleJob', command);
|
||||
jobService.triggerJob(job);
|
||||
jobService.deleteJob(job);
|
||||
|
||||
expect(apiService.get).toHaveBeenCalledWith('/quartz-manager/job-classes');
|
||||
expect(apiService.get).toHaveBeenCalledWith('/quartz-manager/jobs');
|
||||
expect(apiService.get).toHaveBeenCalledWith('/quartz-manager/jobs/DEFAULT/sampleJob');
|
||||
expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/jobs/DEFAULT/sampleJob', {jobClass: 'SampleJob', description: '', durable: true, requestsRecovery: false, jobDataMap: {}});
|
||||
expect(apiService.put).toHaveBeenCalledWith('/quartz-manager/jobs/DEFAULT/sampleJob', {jobClass: 'SampleJob', description: '', durable: true, requestsRecovery: false, jobDataMap: {}});
|
||||
expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/jobs/DEFAULT/sampleJob', command);
|
||||
expect(apiService.put).toHaveBeenCalledWith('/quartz-manager/jobs/DEFAULT/sampleJob', command);
|
||||
expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/jobs/DEFAULT/sampleJob/trigger', {});
|
||||
expect(apiService.delete).toHaveBeenCalledWith('/quartz-manager/jobs/DEFAULT/sampleJob');
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ describe('TriggerService', () => {
|
||||
apiService = {
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
delete: jest.fn()
|
||||
};
|
||||
triggerService = new TriggerService(apiService);
|
||||
@@ -28,4 +29,14 @@ describe('TriggerService', () => {
|
||||
expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/triggers/DEFAULT/sampleTrigger/resume', {});
|
||||
expect(apiService.delete).toHaveBeenCalledWith('/quartz-manager/triggers/DEFAULT/sampleTrigger');
|
||||
});
|
||||
|
||||
it('uses generic trigger create and update endpoints', () => {
|
||||
const command: any = {triggerType: 'CRON', cronExpression: '0 0/5 * * * ?'};
|
||||
|
||||
triggerService.saveTrigger('OPS', 'cronTrigger', command);
|
||||
triggerService.updateTrigger('OPS', 'cronTrigger', command);
|
||||
|
||||
expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/triggers/OPS/cronTrigger', command);
|
||||
expect(apiService.put).toHaveBeenCalledWith('/quartz-manager/triggers/OPS/cronTrigger', command);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {Observable} from 'rxjs';
|
||||
import {Trigger} from '../model/trigger.model';
|
||||
import {TriggerKey} from '../model/triggerKey.model';
|
||||
import {CONTEXT_PATH, getBaseUrl} from './config.service';
|
||||
import {TriggerCommand} from '../model/trigger-command.model';
|
||||
|
||||
@Injectable()
|
||||
export class TriggerService {
|
||||
@@ -20,6 +21,14 @@ export class TriggerService {
|
||||
return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/triggers/${triggerKey.group || 'DEFAULT'}/${triggerKey.name}`);
|
||||
}
|
||||
|
||||
saveTrigger = (group: string, name: string, config: TriggerCommand): Observable<Trigger> => {
|
||||
return this.apiService.post(getBaseUrl() + `${CONTEXT_PATH}/triggers/${group || 'DEFAULT'}/${name}`, config);
|
||||
}
|
||||
|
||||
updateTrigger = (group: string, name: string, config: TriggerCommand): Observable<Trigger> => {
|
||||
return this.apiService.put(getBaseUrl() + `${CONTEXT_PATH}/triggers/${group || 'DEFAULT'}/${name}`, config);
|
||||
}
|
||||
|
||||
pauseTrigger = (triggerKey: TriggerKey): Observable<void> => {
|
||||
return this.apiService.post(getBaseUrl() + `${CONTEXT_PATH}/triggers/${triggerKey.group || 'DEFAULT'}/${triggerKey.name}/pause`, {});
|
||||
}
|
||||
|
||||
@@ -245,17 +245,16 @@
|
||||
|
||||
<div class="page" [class.active]="activePage === 'calendars'">
|
||||
<div class="page-kicker">
|
||||
<div><h2>Calendars</h2><p>Quartz calendar registry, rule editing, trigger usage, and next included time testing are not exposed by the backend yet.</p></div>
|
||||
<div class="toolbar"><input class="search" value="Filter calendars" data-roadmap="Calendar filtering is on the roadmap"><button type="button" class="btn primary" data-roadmap="Calendar creation is on the roadmap">New Calendar</button></div>
|
||||
<div><h2>Calendars</h2><p>Manage Quartz calendar exclusions and inspect which triggers are attached to each calendar.</p></div>
|
||||
<div class="toolbar"><input class="search" name="calendarSearch" placeholder="Filter calendars" [(ngModel)]="calendarSearch"><button type="button" class="btn primary" (click)="openCreateCalendarWizard()">New Calendar</button></div>
|
||||
</div>
|
||||
<section class="card roadmap-card" data-roadmap="Quartz calendar management is on the roadmap">
|
||||
<div class="card-header"><h2 class="card-title">Calendar Registry</h2><span class="chip warn">ROADMAP</span></div>
|
||||
<div class="card-body two-column compact-roadmap">
|
||||
<div>
|
||||
<p class="roadmap-copy">This UI is ready for WeeklyCalendar, HolidayCalendar, MonthlyCalendar, DailyCalendar, and CronCalendar once the API surface is added.</p>
|
||||
<div class="calendar-grid" aria-label="Calendar exclusion preview"><div class="calendar-cell">Mon</div><div class="calendar-cell">Tue</div><div class="calendar-cell">Wed</div><div class="calendar-cell">Thu</div><div class="calendar-cell">Fri</div><div class="calendar-cell excluded">Sat</div><div class="calendar-cell excluded">Sun</div></div>
|
||||
</div>
|
||||
<aside class="filter-panel"><h3>Planned backend</h3><div class="help">List calendars, inspect calendar type/base calendar, attach calendars to triggers, test included time, and edit exclusion rules.</div><button type="button" class="btn" data-roadmap="Calendar trigger usage is on the roadmap">Show Triggers</button><button type="button" class="btn danger" data-roadmap="Calendar deletion is on the roadmap">Delete Calendar</button></aside>
|
||||
<section class="card">
|
||||
<div class="card-header"><h2 class="card-title">Calendar Registry</h2><span class="chip normal">{{ getCalendarRows().length }} / {{ calendars.length }} CALENDARS</span></div>
|
||||
<div class="split">
|
||||
<div class="table-wrap"><table><thead><tr><th>Name</th><th>Type</th><th>Description</th><th>Triggers</th></tr></thead><tbody>@for (calendar of getCalendarRows(); track calendar.name) { <tr class="selectable" [class.selected]="selectedCalendar?.name === calendar.name" (click)="selectCalendar(calendar)"><td class="mono">{{ calendar.name }}</td><td><span class="chip accent">{{ calendar.type }}</span></td><td>{{ calendar.description || '-' }}</td><td class="mono">{{ calendar.triggerKeys?.length || 0 }}</td></tr> } @empty { <tr><td colspan="4">No calendars registered. Create one to exclude time windows from trigger firing.</td></tr> }</tbody></table></div>
|
||||
<aside class="detail drawer detail-drawer" [class.drawer-open]="detailDrawerOpen && activePage === 'calendars'" aria-label="Calendar detail drawer"><div class="drawer-title"><div><span class="chip accent">{{ selectedCalendar?.type || 'NONE' }}</span><h2>{{ selectedCalendar?.name || 'No calendar' }}</h2><div class="caption">{{ selectedCalendar?.description || 'Select a calendar to inspect rules.' }}</div></div><button type="button" class="drawer-close" (click)="closeDetailDrawer()">Close</button></div><div class="field-grid"><div class="field"><label>Type</label><strong>{{ selectedCalendar?.type || '-' }}</strong></div><div class="field"><label>Attached triggers</label><strong>{{ selectedCalendar?.triggerKeys?.length || 0 }}</strong></div><div class="field"><label>Cron</label><strong>{{ selectedCalendar?.cronExpression || '-' }}</strong></div><div class="field"><label>Time zone</label><strong>{{ selectedCalendar?.timeZone || '-' }}</strong></div></div><pre class="code-block">Triggers
|
||||
@for (triggerKey of selectedCalendar?.triggerKeys || []; track triggerKey.group + '.' + triggerKey.name) { {{ triggerKey.group }}.{{ triggerKey.name }}
|
||||
} @empty { none }</pre><div class="control"><label>Included time test</label><input class="input mono" type="datetime-local" name="calendarIncludedTime" [(ngModel)]="calendarDraft.includedTime"></div><div class="help">{{ calendarIncludedTimeResult || 'Test whether this calendar includes a specific timestamp.' }}</div><div class="actions"><button type="button" class="btn" (click)="testSelectedCalendarTime()">Test Time</button><button type="button" class="btn" (click)="openEditCalendarWizard()">Edit</button><button type="button" class="btn danger" (click)="deleteSelectedCalendar()">Delete</button></div></aside>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -328,7 +327,7 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@if (wizardOpen || jobWizardOpen || detailDrawerOpen) {
|
||||
@if (wizardOpen || jobWizardOpen || calendarWizardOpen || detailDrawerOpen) {
|
||||
<button type="button" class="drawer-backdrop" aria-label="Close drawer" (click)="closeDrawers()"></button>
|
||||
}
|
||||
|
||||
@@ -341,17 +340,16 @@
|
||||
}
|
||||
|
||||
<aside class="wizard drawer" [class.drawer-open]="wizardOpen" aria-label="Trigger creation wizard">
|
||||
<div class="wizard-header"><div><h2>{{ getWizardTitle() }}</h2><div class="caption">SimpleTrigger is supported now. Other trigger types are roadmap-gated.</div></div><button type="button" class="drawer-close" (click)="closeWizardDrawer()">Close</button></div>
|
||||
<div class="wizard-header"><div><h2>{{ getWizardTitle() }}</h2><div class="caption">Simple, Cron, Daily Time Interval, and Calendar Interval triggers are supported.</div></div><button type="button" class="drawer-close" (click)="closeWizardDrawer()">Close</button></div>
|
||||
<div class="stepper"><div class="step done"><span></span><span>Identity</span></div><div class="step active"><span></span><span>Type</span></div><div class="step done"><span></span><span>Schedule</span></div><div class="step done"><span></span><span>Advanced</span></div><div class="step active"><span></span><span>Preview</span></div></div>
|
||||
<form class="wizard-form" (ngSubmit)="submitTriggerWizard()">
|
||||
<div class="wizard-scroll">
|
||||
@if (wizardError) { <div class="warning-box"><strong>Unable to save</strong><span>{{ wizardError }}</span></div> }
|
||||
<section class="form-card"><h3>Identity</h3><div class="form-section"><div class="control"><label>Trigger key</label><div class="input-row"><input class="input" name="triggerName" [(ngModel)]="triggerDraft.triggerName" [readonly]="wizardMode === 'edit'" required><input class="input mono" name="group" [(ngModel)]="triggerDraft.group" list="trigger-groups" required></div><datalist id="trigger-groups">@for (group of getTriggerGroups(); track group) { <option [value]="group"></option> }</datalist><div class="help">Quartz groups are implicit namespaces. Type a new group to create it with this trigger.</div></div><div class="control"><label>Target type</label><select class="select" name="jobTargetType" [(ngModel)]="triggerDraft.jobTargetType"><option value="stored">Existing stored job</option><option value="class">New job from class</option></select></div>@if (triggerDraft.jobTargetType === 'stored') { <div class="control"><label>Stored job</label><select class="select" name="storedJobKey" [(ngModel)]="triggerDraft.storedJobKey" required>@for (job of getStoredJobOptions(); track job.value) { <option [value]="job.value">{{ job.label }}</option> }</select><div class="help">The trigger will call TriggerBuilder.forJob with this stored job key.</div></div> } @else { <div class="control"><label>Job class</label><select class="select" name="jobClass" [(ngModel)]="triggerDraft.jobClass" required>@for (job of jobs; track job) { <option [value]="job">{{ job }}</option> }</select><div class="help">The backend will create an ephemeral job for this trigger.</div></div> }</div></section>
|
||||
<section class="form-card"><h3>Trigger Type</h3><div class="form-section"><div class="radio-grid"><div class="type-option active"><strong>Simple</strong><span class="help">Repeat every fixed interval. Supported now.</span></div><button type="button" class="type-option" data-roadmap="CronTrigger support is on the roadmap"><strong>Cron</strong><span class="help">Calendar expression builder.</span></button><button type="button" class="type-option" data-roadmap="DailyTimeIntervalTrigger support is on the roadmap"><strong>Daily Time</strong><span class="help">Run in a daily time window.</span></button><button type="button" class="type-option" data-roadmap="CalendarIntervalTrigger support is on the roadmap"><strong>Calendar Interval</strong><span class="help">Every N days, weeks, months.</span></button></div></div></section>
|
||||
<section class="form-card"><h3>Schedule Editor</h3><div class="form-section"><div class="control"><label>Start</label><input class="input mono" type="datetime-local" name="startDate" [(ngModel)]="triggerDraft.startDate"></div><div class="control"><label>Repeat interval</label><div class="input-row"><input class="input mono" type="number" min="1" name="repeatIntervalAmount" [(ngModel)]="triggerDraft.repeatIntervalAmount" required><select class="select" name="repeatIntervalUnit" [(ngModel)]="triggerDraft.repeatIntervalUnit"><option value="milliseconds">milliseconds</option><option value="seconds">seconds</option><option value="minutes">minutes</option><option value="hours">hours</option><option value="days">days</option></select></div><div class="help">The UI edits operational units and persists the current backend repeatInterval in milliseconds.</div></div><div class="control"><label>Repeat count</label><input class="input mono" type="number" name="repeatCount" [(ngModel)]="triggerDraft.repeatCount" required><div class="help">Use -1 to repeat indefinitely.</div></div><div class="control"><label>End</label><input class="input mono" type="datetime-local" name="endDate" [(ngModel)]="triggerDraft.endDate"></div></div></section>
|
||||
<section class="form-card"><h3>Advanced</h3><div class="form-section"><div class="control"><label>Misfire policy</label><select class="select" name="misfireInstruction" [(ngModel)]="triggerDraft.misfireInstruction" required><option value="MISFIRE_INSTRUCTION_FIRE_NOW">MISFIRE_INSTRUCTION_FIRE_NOW</option><option value="MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT">RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT</option><option value="MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT">RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT</option><option value="MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT">RESCHEDULE_NEXT_WITH_REMAINING_COUNT</option><option value="MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT">RESCHEDULE_NEXT_WITH_EXISTING_COUNT</option></select></div><div class="control"><label>JobDataMap override</label><div class="data-map-editor">@for (entry of triggerDraft.jobDataMapEntries; track $index) { <div class="data-map-row"><input class="input mono" name="triggerDataKey{{$index}}" placeholder="key" [(ngModel)]="entry.key"><select class="select" name="triggerDataType{{$index}}" [(ngModel)]="entry.type"><option value="string">string</option><option value="number">number</option><option value="boolean">boolean</option><option value="json">json</option><option value="null">null</option></select><input class="input mono" name="triggerDataValue{{$index}}" placeholder="value" [(ngModel)]="entry.value" [readonly]="entry.type === 'null'"><button type="button" class="btn danger compact" (click)="removeJobDataMapEntry(triggerDraft.jobDataMapEntries, $index)">Remove</button></div> }</div><button type="button" class="btn" (click)="addJobDataMapEntry(triggerDraft.jobDataMapEntries)">Add Data</button><pre class="code-block">{{ getTriggerDraftDataMapPreview() }}</pre></div></div></section>
|
||||
<section class="preview"><h4>Plain-language summary</h4><div>Run <strong>{{ triggerDraft.jobTargetType === 'stored' ? triggerDraft.storedJobKey.replace('::', '.') : shortClassName(triggerDraft.jobClass) || 'selected job' }}</strong> every <strong>{{ triggerDraft.repeatIntervalAmount }} {{ triggerDraft.repeatIntervalUnit }}</strong>, starting at <strong>{{ triggerDraft.startDate || 'backend default start time' }}</strong>.</div><div class="fire-list">@for (fireTime of getFirePreview(); track fireTime) { <span>{{ fireTime }}</span> }</div></section>
|
||||
<div class="warning-box"><strong>Backend support boundary</strong><span>SimpleTrigger create/reschedule is submitted. Groups are implicit Quartz namespaces, not standalone records.</span></div>
|
||||
<section class="form-card"><h3>Trigger Type</h3><div class="form-section"><div class="radio-grid"><button type="button" class="type-option" [class.active]="triggerDraft.triggerType === 'SIMPLE'" (click)="selectTriggerType('SIMPLE')"><strong>Simple</strong><span class="help">Repeat every fixed interval.</span></button><button type="button" class="type-option" [class.active]="triggerDraft.triggerType === 'CRON'" (click)="selectTriggerType('CRON')"><strong>Cron</strong><span class="help">Cron expression schedule.</span></button><button type="button" class="type-option" [class.active]="triggerDraft.triggerType === 'DAILY_TIME_INTERVAL'" (click)="selectTriggerType('DAILY_TIME_INTERVAL')"><strong>Daily Time</strong><span class="help">Run in a daily time window.</span></button><button type="button" class="type-option" [class.active]="triggerDraft.triggerType === 'CALENDAR_INTERVAL'" (click)="selectTriggerType('CALENDAR_INTERVAL')"><strong>Calendar Interval</strong><span class="help">Every N calendar units.</span></button></div></div></section>
|
||||
<section class="form-card"><h3>Schedule Editor</h3><div class="form-section"><div class="control"><label>Start</label><input class="input mono" type="datetime-local" name="startDate" [(ngModel)]="triggerDraft.startDate"></div><div class="control"><label>End</label><input class="input mono" type="datetime-local" name="endDate" [(ngModel)]="triggerDraft.endDate"></div>@if (triggerDraft.triggerType === 'CRON') { <div class="control"><label>Cron expression</label><input class="input mono" name="cronExpression" [(ngModel)]="triggerDraft.cronExpression" required><div class="help">Quartz cron format, for example 0 0/5 * * * ?</div></div><div class="control"><label>Timezone</label><input class="input mono" name="cronTimeZone" [(ngModel)]="triggerDraft.timeZone"></div> } @else { <div class="control"><label>Interval</label><div class="input-row"><input class="input mono" type="number" min="1" name="repeatIntervalAmount" [(ngModel)]="triggerDraft.repeatIntervalAmount" required><select class="select" name="repeatIntervalUnit" [(ngModel)]="triggerDraft.repeatIntervalUnit"><option value="milliseconds" [disabled]="triggerDraft.triggerType !== 'SIMPLE'">milliseconds</option><option value="seconds">seconds</option><option value="minutes">minutes</option><option value="hours">hours</option><option value="days">days</option><option value="weeks" [disabled]="triggerDraft.triggerType !== 'CALENDAR_INTERVAL'">weeks</option><option value="months" [disabled]="triggerDraft.triggerType !== 'CALENDAR_INTERVAL'">months</option><option value="years" [disabled]="triggerDraft.triggerType !== 'CALENDAR_INTERVAL'">years</option></select></div></div> } @if (triggerDraft.triggerType === 'SIMPLE') { <div class="control"><label>Repeat count</label><input class="input mono" type="number" name="repeatCount" [(ngModel)]="triggerDraft.repeatCount" required><div class="help">Use -1 to repeat indefinitely.</div></div> } @if (triggerDraft.triggerType === 'DAILY_TIME_INTERVAL') { <div class="control"><label>Daily window</label><div class="input-row"><input class="input mono" name="startTimeOfDay" [(ngModel)]="triggerDraft.startTimeOfDay"><input class="input mono" name="endTimeOfDay" [(ngModel)]="triggerDraft.endTimeOfDay"></div></div><div class="control"><label>Days of week</label><div class="command-row">@for (day of [1,2,3,4,5,6,7]; track day) { <button type="button" class="btn compact" [class.primary]="isDayOfWeekSelected(day)" (click)="toggleDayOfWeek(day)">{{ day }}</button> }</div><div class="help">Quartz uses 1=Sunday through 7=Saturday.</div></div> } @if (triggerDraft.triggerType === 'CALENDAR_INTERVAL') { <label class="check-row"><input type="checkbox" name="preserveHour" [(ngModel)]="triggerDraft.preserveHourOfDayAcrossDaylightSavings"> Preserve hour across daylight saving</label><label class="check-row"><input type="checkbox" name="skipMissingHour" [(ngModel)]="triggerDraft.skipDayIfHourDoesNotExist"> Skip day if hour does not exist</label><div class="control"><label>Timezone</label><input class="input mono" name="calendarIntervalTimeZone" [(ngModel)]="triggerDraft.timeZone"></div> }</div></section>
|
||||
<section class="form-card"><h3>Advanced</h3><div class="form-section"><div class="control"><label>Calendar</label><select class="select" name="calendarName" [(ngModel)]="triggerDraft.calendarName"><option value="">No calendar</option>@for (calendarName of getCalendarOptions(); track calendarName) { <option [value]="calendarName">{{ calendarName }}</option> }</select></div><div class="control"><label>Misfire policy</label><select class="select" name="misfireInstruction" [(ngModel)]="triggerDraft.misfireInstruction" required>@if (triggerDraft.triggerType === 'SIMPLE') { <option value="MISFIRE_INSTRUCTION_FIRE_NOW">FIRE_NOW</option><option value="MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT">RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT</option><option value="MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT">RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT</option><option value="MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT">RESCHEDULE_NEXT_WITH_REMAINING_COUNT</option><option value="MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT">RESCHEDULE_NEXT_WITH_EXISTING_COUNT</option> } @else { <option value="FIRE_AND_PROCEED">FIRE_AND_PROCEED</option><option value="DO_NOTHING">DO_NOTHING</option><option value="IGNORE_MISFIRES">IGNORE_MISFIRES</option> }</select></div><div class="control"><label>JobDataMap override</label><div class="data-map-editor">@for (entry of triggerDraft.jobDataMapEntries; track $index) { <div class="data-map-row"><input class="input mono" name="triggerDataKey{{$index}}" placeholder="key" [(ngModel)]="entry.key"><select class="select" name="triggerDataType{{$index}}" [(ngModel)]="entry.type"><option value="string">string</option><option value="number">number</option><option value="boolean">boolean</option><option value="json">json</option><option value="null">null</option></select><input class="input mono" name="triggerDataValue{{$index}}" placeholder="value" [(ngModel)]="entry.value" [readonly]="entry.type === 'null'"><button type="button" class="btn danger compact" (click)="removeJobDataMapEntry(triggerDraft.jobDataMapEntries, $index)">Remove</button></div> }</div><button type="button" class="btn" (click)="addJobDataMapEntry(triggerDraft.jobDataMapEntries)">Add Data</button><pre class="code-block">{{ getTriggerDraftDataMapPreview() }}</pre></div></div></section>
|
||||
<section class="preview"><h4>Plain-language summary</h4><div>Run <strong>{{ triggerDraft.jobTargetType === 'stored' ? triggerDraft.storedJobKey.replace('::', '.') : shortClassName(triggerDraft.jobClass) || 'selected job' }}</strong> as a <strong>{{ triggerDraft.triggerType }}</strong> trigger, starting at <strong>{{ triggerDraft.startDate || 'backend default start time' }}</strong>.</div><div class="fire-list">@for (fireTime of getFirePreview(); track fireTime) { <span>{{ fireTime }}</span> }</div></section>
|
||||
</div>
|
||||
<div class="wizard-footer"><button type="button" class="btn" (click)="resetWizard()">Reset</button><button type="submit" class="btn primary" [disabled]="wizardSubmitting || !canSubmitTrigger()">{{ wizardSubmitting ? 'Saving...' : getWizardCta() }}</button></div>
|
||||
</form>
|
||||
@@ -369,4 +367,16 @@
|
||||
<div class="wizard-footer"><button type="button" class="btn" (click)="closeJobWizardDrawer()">Cancel</button><button type="submit" class="btn primary" [disabled]="jobWizardSubmitting || !canSubmitJob()">{{ jobWizardSubmitting ? 'Saving...' : jobWizardMode === 'edit' ? 'Save Job' : 'Create Job' }}</button></div>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
<aside class="wizard drawer" [class.drawer-open]="calendarWizardOpen" aria-label="Calendar editor">
|
||||
<div class="wizard-header"><div><h2>{{ calendarWizardMode === 'edit' ? 'Edit Calendar' : 'New Calendar' }}</h2><div class="caption">Quartz calendars exclude times from trigger schedules.</div></div><button type="button" class="drawer-close" (click)="closeCalendarWizardDrawer()">Close</button></div>
|
||||
<form class="wizard-form" (ngSubmit)="submitCalendarWizard()">
|
||||
<div class="wizard-scroll">
|
||||
@if (calendarWizardError) { <div class="warning-box"><strong>Unable to save</strong><span>{{ calendarWizardError }}</span></div> }
|
||||
<section class="form-card"><h3>Identity</h3><div class="form-section"><div class="control"><label>Calendar name</label><input class="input mono" name="calendarNameInput" [(ngModel)]="calendarDraft.name" [readonly]="calendarWizardMode === 'edit'" required></div><div class="control"><label>Type</label><select class="select" name="calendarType" [(ngModel)]="calendarDraft.type"><option value="WEEKLY">Weekly</option><option value="MONTHLY">Monthly</option><option value="ANNUAL">Annual</option><option value="HOLIDAY">Holiday</option><option value="DAILY">Daily</option><option value="CRON">Cron</option></select></div><div class="control"><label>Description</label><input class="input" name="calendarDescription" [(ngModel)]="calendarDraft.description"></div></div></section>
|
||||
<section class="form-card"><h3>Rules</h3><div class="form-section">@if (getCalendarRuleMode() === 'weekdays') { <div class="control"><label>Excluded days</label><div class="command-row">@for (day of [1,2,3,4,5,6,7]; track day) { <button type="button" class="btn compact" [class.primary]="calendarDraft.excludedDaysOfWeek.includes(day)" (click)="toggleCalendarWeekday(day)">{{ day }}</button> }</div><div class="help">Quartz uses 1=Sunday through 7=Saturday.</div></div> } @if (getCalendarRuleMode() === 'monthdays') { <div class="control"><label>Excluded month days</label><div class="command-row">@for (day of [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]; track day) { <button type="button" class="btn compact" [class.primary]="calendarDraft.excludedDaysOfMonth.includes(day)" (click)="toggleCalendarMonthday(day)">{{ day }}</button> }</div></div> } @if (getCalendarRuleMode() === 'dates') { <div class="control"><label>Excluded dates</label><div class="data-map-editor">@for (date of calendarDraft.excludedDates; track $index) { <div class="data-map-row"><input class="input mono" type="datetime-local" name="calendarDate{{$index}}" [(ngModel)]="calendarDraft.excludedDates[$index]"><button type="button" class="btn danger compact" (click)="removeCalendarDate($index)">Remove</button></div> }</div><button type="button" class="btn" (click)="addCalendarDate()">Add Date</button></div> } @if (getCalendarRuleMode() === 'timeRange') { <div class="control"><label>Excluded time range</label><div class="input-row"><input class="input mono" name="rangeStartingTime" [(ngModel)]="calendarDraft.rangeStartingTime"><input class="input mono" name="rangeEndingTime" [(ngModel)]="calendarDraft.rangeEndingTime"></div></div><label class="check-row"><input type="checkbox" name="invertTimeRange" [(ngModel)]="calendarDraft.invertTimeRange"> Invert time range</label> } @if (getCalendarRuleMode() === 'cron') { <div class="control"><label>Cron exclusion expression</label><input class="input mono" name="calendarCron" [(ngModel)]="calendarDraft.cronExpression" required></div><div class="control"><label>Timezone</label><input class="input mono" name="calendarTimeZone" [(ngModel)]="calendarDraft.timeZone"></div> }</div></section>
|
||||
</div>
|
||||
<div class="wizard-footer"><button type="button" class="btn" (click)="closeCalendarWizardDrawer()">Cancel</button><button type="submit" class="btn primary" [disabled]="calendarWizardSubmitting || !canSubmitCalendar()">{{ calendarWizardSubmitting ? 'Saving...' : calendarWizardMode === 'edit' ? 'Save Calendar' : 'Create Calendar' }}</button></div>
|
||||
</form>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,14 @@ import {Component, NgZone, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
import {SchedulerService, TriggerService} from '../../services';
|
||||
import {CalendarService, SchedulerService, TriggerService} from '../../services';
|
||||
import JobService from '../../services/job.service';
|
||||
import {LogsRxWebsocketService} from '../../services/logs.rx-websocket.service';
|
||||
import {ProgressRxWebsocketService} from '../../services/progress.rx-websocket.service';
|
||||
import {Scheduler} from '../../model/scheduler.model';
|
||||
import {SimpleTriggerCommand} from '../../model/simple-trigger.command';
|
||||
import {SimpleTrigger} from '../../model/simple-trigger.model';
|
||||
import {Trigger} from '../../model/trigger.model';
|
||||
import {TriggerCommand, TriggerType} from '../../model/trigger-command.model';
|
||||
import {CalendarType, QuartzCalendar} from '../../model/calendar.model';
|
||||
import {ScheduledJob} from '../../model/scheduled-job.model';
|
||||
import {ScheduledJobCommand} from '../../model/scheduled-job.command';
|
||||
import {TriggerKey} from '../../model/triggerKey.model';
|
||||
@@ -18,6 +19,7 @@ type ConsolePage = 'dashboard' | 'jobs' | 'triggers' | 'calendars' | 'executions
|
||||
type WizardMode = 'create' | 'edit';
|
||||
type JobTargetType = 'stored' | 'class';
|
||||
type JobDataMapType = 'string' | 'number' | 'boolean' | 'json' | 'null';
|
||||
type CalendarRuleMode = 'dates' | 'weekdays' | 'monthdays' | 'timeRange' | 'cron';
|
||||
|
||||
interface ConsoleLogRecord {
|
||||
time: Date;
|
||||
@@ -30,6 +32,7 @@ interface ConsoleLogRecord {
|
||||
interface TriggerDraft {
|
||||
triggerName: string;
|
||||
group: string;
|
||||
triggerType: TriggerType;
|
||||
jobTargetType: JobTargetType;
|
||||
storedJobKey: string;
|
||||
jobClass: string;
|
||||
@@ -39,9 +42,32 @@ interface TriggerDraft {
|
||||
repeatIntervalUnit: string;
|
||||
repeatCount: number;
|
||||
misfireInstruction: string;
|
||||
cronExpression: string;
|
||||
timeZone: string;
|
||||
startTimeOfDay: string;
|
||||
endTimeOfDay: string;
|
||||
daysOfWeek: number[];
|
||||
preserveHourOfDayAcrossDaylightSavings: boolean;
|
||||
skipDayIfHourDoesNotExist: boolean;
|
||||
calendarName: string;
|
||||
jobDataMapEntries: JobDataMapEntry[];
|
||||
}
|
||||
|
||||
interface CalendarDraft {
|
||||
name: string;
|
||||
type: CalendarType;
|
||||
description: string;
|
||||
cronExpression: string;
|
||||
timeZone: string;
|
||||
rangeStartingTime: string;
|
||||
rangeEndingTime: string;
|
||||
invertTimeRange: boolean;
|
||||
excludedDaysOfWeek: number[];
|
||||
excludedDaysOfMonth: number[];
|
||||
excludedDates: string[];
|
||||
includedTime: string;
|
||||
}
|
||||
|
||||
interface JobDraft {
|
||||
name: string;
|
||||
group: string;
|
||||
@@ -73,9 +99,9 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
scheduler: Scheduler;
|
||||
schedulerLoading = false;
|
||||
triggerKeys: TriggerKey[] = [];
|
||||
triggerDetailsByName: {[triggerName: string]: SimpleTrigger} = {};
|
||||
triggerDetailsByName: {[triggerName: string]: Trigger} = {};
|
||||
selectedTriggerKey: TriggerKey;
|
||||
selectedTrigger: SimpleTrigger;
|
||||
selectedTrigger: Trigger;
|
||||
selectedJobClass: string;
|
||||
selectedScheduledJob: ScheduledJob;
|
||||
jobs: string[] = [];
|
||||
@@ -95,14 +121,23 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
wizardError: string;
|
||||
jobWizardError: string;
|
||||
triggerDraft: TriggerDraft = this.buildEmptyDraft();
|
||||
calendarDraft: CalendarDraft = this.buildEmptyCalendarDraft();
|
||||
jobDraft: JobDraft = this.buildEmptyJobDraft();
|
||||
jobWizardMode: WizardMode = 'create';
|
||||
jobGroupFilter = 'ALL';
|
||||
triggerGroupFilter = 'ALL';
|
||||
jobSearch = '';
|
||||
triggerSearch = '';
|
||||
calendarSearch = '';
|
||||
calendars: QuartzCalendar[] = [];
|
||||
selectedCalendar: QuartzCalendar;
|
||||
calendarWizardOpen = false;
|
||||
calendarWizardMode: WizardMode = 'create';
|
||||
calendarWizardSubmitting = false;
|
||||
calendarWizardError: string;
|
||||
calendarIncludedTimeResult: string;
|
||||
|
||||
private readonly roadmapPages = new Set<ConsolePage>(['calendars', 'executions']);
|
||||
private readonly roadmapPages = new Set<ConsolePage>(['executions']);
|
||||
private readonly subscriptions: Subscription[] = [];
|
||||
private logsSubscription: Subscription;
|
||||
private progressSubscription: Subscription;
|
||||
@@ -111,6 +146,7 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private schedulerService: SchedulerService,
|
||||
private triggerService: TriggerService,
|
||||
private calendarService: CalendarService,
|
||||
private jobService: JobService,
|
||||
private logsRxWebsocketService: LogsRxWebsocketService,
|
||||
private progressRxWebsocketService: ProgressRxWebsocketService,
|
||||
@@ -122,6 +158,7 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
this.fetchTriggers();
|
||||
this.fetchJobs();
|
||||
this.fetchScheduledJobs();
|
||||
this.fetchCalendars();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -189,10 +226,15 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
this.jobWizardOpen = false;
|
||||
}
|
||||
|
||||
closeCalendarWizardDrawer() {
|
||||
this.calendarWizardOpen = false;
|
||||
}
|
||||
|
||||
closeDrawers() {
|
||||
this.detailDrawerOpen = false;
|
||||
this.wizardOpen = false;
|
||||
this.jobWizardOpen = false;
|
||||
this.calendarWizardOpen = false;
|
||||
}
|
||||
|
||||
refreshScheduler() {
|
||||
@@ -294,10 +336,21 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
this.subscriptions.push(subscription);
|
||||
}
|
||||
|
||||
fetchCalendars() {
|
||||
const subscription = this.calendarService.fetchCalendars().subscribe({
|
||||
next: calendars => {
|
||||
this.calendars = calendars || [];
|
||||
this.selectedCalendar = this.selectedCalendar || this.calendars[0];
|
||||
},
|
||||
error: () => this.operationError = 'Unable to load calendars.'
|
||||
});
|
||||
this.subscriptions.push(subscription);
|
||||
}
|
||||
|
||||
fetchTriggerDetails(triggerKeys: TriggerKey[]) {
|
||||
triggerKeys.forEach(triggerKey => {
|
||||
const subscription = this.schedulerService.getSimpleTriggerConfig(triggerKey.name, this.getTriggerGroup(triggerKey)).subscribe({
|
||||
next: trigger => this.triggerDetailsByName[this.getTriggerDetailKey(triggerKey)] = trigger as SimpleTrigger,
|
||||
const subscription = this.triggerService.getTrigger(triggerKey).subscribe({
|
||||
next: trigger => this.triggerDetailsByName[this.getTriggerDetailKey(triggerKey)] = trigger,
|
||||
error: () => {
|
||||
this.triggerDetailsByName[this.getTriggerDetailKey(triggerKey)] = null;
|
||||
}
|
||||
@@ -317,15 +370,15 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
this.triggerLoading = true;
|
||||
this.selectedTrigger = this.triggerDetailsByName[this.getTriggerDetailKey(triggerKey)] || null;
|
||||
this.subscribeToTriggerTopics(this.selectedTriggerKey);
|
||||
const subscription = this.schedulerService.getSimpleTriggerConfig(triggerKey.name, this.getTriggerGroup(triggerKey)).subscribe({
|
||||
const subscription = this.triggerService.getTrigger(triggerKey).subscribe({
|
||||
next: trigger => {
|
||||
this.selectedTrigger = trigger as SimpleTrigger;
|
||||
this.triggerDetailsByName[this.getTriggerDetailKey(triggerKey)] = trigger as SimpleTrigger;
|
||||
this.selectedTrigger = trigger;
|
||||
this.triggerDetailsByName[this.getTriggerDetailKey(triggerKey)] = trigger;
|
||||
this.triggerLoading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.triggerLoading = false;
|
||||
this.showRoadmapNotice('Only SimpleTrigger details are supported by the current backend');
|
||||
this.operationError = 'Unable to load trigger details.';
|
||||
}
|
||||
});
|
||||
this.subscriptions.push(subscription);
|
||||
@@ -452,6 +505,95 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
this.subscriptions.push(subscription);
|
||||
}
|
||||
|
||||
selectCalendar(calendar: QuartzCalendar) {
|
||||
this.selectedCalendar = calendar;
|
||||
this.openDetailDrawer();
|
||||
}
|
||||
|
||||
openCreateCalendarWizard() {
|
||||
this.calendarWizardMode = 'create';
|
||||
this.calendarWizardError = null;
|
||||
this.calendarDraft = this.buildEmptyCalendarDraft();
|
||||
this.calendarWizardOpen = true;
|
||||
this.detailDrawerOpen = false;
|
||||
this.selectPage('calendars');
|
||||
this.calendarWizardOpen = true;
|
||||
}
|
||||
|
||||
openEditCalendarWizard() {
|
||||
if (!this.selectedCalendar) {
|
||||
return;
|
||||
}
|
||||
this.calendarWizardMode = 'edit';
|
||||
this.calendarWizardError = null;
|
||||
this.calendarDraft = this.fromCalendarToDraft(this.selectedCalendar);
|
||||
this.calendarWizardOpen = true;
|
||||
this.detailDrawerOpen = false;
|
||||
this.selectPage('calendars');
|
||||
this.calendarWizardOpen = true;
|
||||
}
|
||||
|
||||
submitCalendarWizard() {
|
||||
this.calendarWizardError = null;
|
||||
if (!this.canSubmitCalendar()) {
|
||||
this.calendarWizardError = 'Calendar name, type, and rule fields are required.';
|
||||
return;
|
||||
}
|
||||
|
||||
const calendar = this.fromCalendarDraftToCommand();
|
||||
const name = this.calendarDraft.name.trim();
|
||||
this.calendarWizardSubmitting = true;
|
||||
const request = this.calendarWizardMode === 'edit'
|
||||
? this.calendarService.updateCalendar(name, calendar)
|
||||
: this.calendarService.createCalendar(name, calendar);
|
||||
|
||||
const subscription = request.subscribe({
|
||||
next: savedCalendar => {
|
||||
this.calendarWizardSubmitting = false;
|
||||
this.upsertCalendar(savedCalendar);
|
||||
this.selectedCalendar = savedCalendar;
|
||||
this.calendarWizardOpen = false;
|
||||
this.detailDrawerOpen = true;
|
||||
this.operationNotice = this.calendarWizardMode === 'edit' ? 'Calendar updated.' : 'Calendar created.';
|
||||
},
|
||||
error: () => {
|
||||
this.calendarWizardSubmitting = false;
|
||||
this.calendarWizardError = 'Unable to save the calendar.';
|
||||
}
|
||||
});
|
||||
this.subscriptions.push(subscription);
|
||||
}
|
||||
|
||||
deleteSelectedCalendar() {
|
||||
if (!this.selectedCalendar || !window.confirm(`Delete calendar ${this.selectedCalendar.name}?`)) {
|
||||
return;
|
||||
}
|
||||
const calendarName = this.selectedCalendar.name;
|
||||
const subscription = this.calendarService.deleteCalendar(calendarName).subscribe({
|
||||
next: () => {
|
||||
this.calendars = this.calendars.filter(calendar => calendar.name !== calendarName);
|
||||
this.selectedCalendar = this.calendars[0];
|
||||
this.operationNotice = 'Calendar deleted.';
|
||||
},
|
||||
error: () => this.operationError = 'Unable to delete the selected calendar.'
|
||||
});
|
||||
this.subscriptions.push(subscription);
|
||||
}
|
||||
|
||||
testSelectedCalendarTime() {
|
||||
if (!this.selectedCalendar) {
|
||||
return;
|
||||
}
|
||||
const testTime = this.fromDatetimeLocalValue(this.calendarDraft.includedTime) || new Date();
|
||||
const subscription = this.calendarService.testIncludedTime(this.selectedCalendar.name, testTime).subscribe({
|
||||
next: result => this.calendarIncludedTimeResult = result.included
|
||||
? 'Included at the tested time.'
|
||||
: `Excluded. Next included time: ${this.formatDateTime(result.nextIncludedTime) || '-'}`,
|
||||
error: () => this.operationError = 'Unable to test the selected calendar.'
|
||||
});
|
||||
this.subscriptions.push(subscription);
|
||||
}
|
||||
|
||||
pauseSelectedTrigger() {
|
||||
if (!this.selectedTriggerKey) {
|
||||
return;
|
||||
@@ -501,7 +643,7 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
this.resetWizard();
|
||||
this.wizardOpen = true;
|
||||
this.detailDrawerOpen = false;
|
||||
this.selectPage('dashboard');
|
||||
this.selectPage('triggers');
|
||||
this.wizardOpen = true;
|
||||
}
|
||||
|
||||
@@ -510,7 +652,7 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
this.selectTrigger(triggerKey, false);
|
||||
}
|
||||
if (!this.selectedTrigger && !this.selectedTriggerKey) {
|
||||
this.showRoadmapNotice('Reschedule requires a SimpleTrigger loaded from the backend');
|
||||
this.showRoadmapNotice('Reschedule requires a trigger loaded from the backend');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -522,18 +664,29 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
this.triggerDraft = {
|
||||
triggerName: this.selectedTriggerKey.name,
|
||||
group: this.selectedTriggerKey.group || 'DEFAULT',
|
||||
triggerType: this.getTriggerTypeValue(trigger),
|
||||
jobTargetType: trigger?.jobKeyDTO ? 'stored' : 'class',
|
||||
storedJobKey: trigger?.jobKeyDTO ? this.getJobOptionValue(trigger.jobKeyDTO.group, trigger.jobKeyDTO.name) : this.getDefaultStoredJobKey(),
|
||||
storedJobKey: trigger?.jobKeyDTO
|
||||
? this.getJobOptionValue(trigger.jobKeyDTO.group, trigger.jobKeyDTO.name)
|
||||
: this.getDefaultStoredJobKey(),
|
||||
jobClass: trigger?.jobDetailDTO?.jobClassName || this.jobs[0] || '',
|
||||
startDate: this.toDatetimeLocalValue(trigger?.startTime),
|
||||
endDate: this.toDatetimeLocalValue(trigger?.endTime),
|
||||
repeatIntervalAmount: repeatInterval.amount,
|
||||
repeatIntervalUnit: repeatInterval.unit,
|
||||
repeatCount: trigger?.repeatCount ?? -1,
|
||||
misfireInstruction: this.getMisfireInstructionName(trigger?.misfireInstruction),
|
||||
misfireInstruction: this.getMisfireInstructionName(trigger?.misfireInstruction, this.getTriggerTypeValue(trigger)),
|
||||
cronExpression: trigger?.cronExpression || '0 0/5 * * * ?',
|
||||
timeZone: trigger?.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
startTimeOfDay: trigger?.startTimeOfDay || '08:00:00',
|
||||
endTimeOfDay: trigger?.endTimeOfDay || '18:00:00',
|
||||
daysOfWeek: trigger?.daysOfWeek || [2, 3, 4, 5, 6],
|
||||
preserveHourOfDayAcrossDaylightSavings: !!trigger?.preserveHourOfDayAcrossDaylightSavings,
|
||||
skipDayIfHourDoesNotExist: !!trigger?.skipDayIfHourDoesNotExist,
|
||||
calendarName: trigger?.calendarName || '',
|
||||
jobDataMapEntries: this.toJobDataMapEntries(trigger?.jobDataMap)
|
||||
};
|
||||
this.selectPage('dashboard');
|
||||
this.selectPage('triggers');
|
||||
this.wizardOpen = true;
|
||||
}
|
||||
|
||||
@@ -546,20 +699,32 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
submitTriggerWizard() {
|
||||
this.wizardError = null;
|
||||
if (!this.canSubmitTrigger()) {
|
||||
this.wizardError = 'Trigger name, job class, misfire policy, and both repeat fields are required for the current backend.';
|
||||
this.wizardError = 'Trigger name, target job, type, and schedule fields are required.';
|
||||
return;
|
||||
}
|
||||
|
||||
const command = new SimpleTriggerCommand();
|
||||
command.triggerName = this.triggerDraft.triggerName.trim();
|
||||
command.triggerGroup = this.triggerDraft.group || 'DEFAULT';
|
||||
const command = new TriggerCommand();
|
||||
command.triggerType = this.triggerDraft.triggerType;
|
||||
command.jobClass = this.triggerDraft.jobTargetType === 'class' ? this.triggerDraft.jobClass : null;
|
||||
command.jobKey = this.triggerDraft.jobTargetType === 'stored' ? this.parseJobOptionValue(this.triggerDraft.storedJobKey) : null;
|
||||
command.startDate = this.fromDatetimeLocalValue(this.triggerDraft.startDate);
|
||||
command.endDate = this.fromDatetimeLocalValue(this.triggerDraft.endDate);
|
||||
command.repeatInterval = this.getRepeatIntervalMs();
|
||||
command.repeatCount = this.triggerDraft.repeatCount;
|
||||
command.repeatInterval = this.getTriggerCommandRepeatInterval();
|
||||
command.repeatCount = this.triggerDraft.triggerType === 'SIMPLE' ? this.triggerDraft.repeatCount : null;
|
||||
command.repeatIntervalUnit = this.getTriggerCommandRepeatIntervalUnit();
|
||||
command.misfireInstruction = this.triggerDraft.misfireInstruction;
|
||||
command.cronExpression = this.triggerDraft.triggerType === 'CRON' ? this.triggerDraft.cronExpression : null;
|
||||
command.timeZone = this.triggerDraft.timeZone;
|
||||
command.startTimeOfDay = this.triggerDraft.triggerType === 'DAILY_TIME_INTERVAL' ? this.triggerDraft.startTimeOfDay : null;
|
||||
command.endTimeOfDay = this.triggerDraft.triggerType === 'DAILY_TIME_INTERVAL' ? this.triggerDraft.endTimeOfDay : null;
|
||||
command.daysOfWeek = this.triggerDraft.triggerType === 'DAILY_TIME_INTERVAL' ? this.triggerDraft.daysOfWeek : null;
|
||||
command.preserveHourOfDayAcrossDaylightSavings = this.triggerDraft.triggerType === 'CALENDAR_INTERVAL'
|
||||
? this.triggerDraft.preserveHourOfDayAcrossDaylightSavings
|
||||
: null;
|
||||
command.skipDayIfHourDoesNotExist = this.triggerDraft.triggerType === 'CALENDAR_INTERVAL'
|
||||
? this.triggerDraft.skipDayIfHourDoesNotExist
|
||||
: null;
|
||||
command.calendarName = this.triggerDraft.calendarName || null;
|
||||
try {
|
||||
command.jobDataMap = this.serializeJobDataMap(this.triggerDraft.jobDataMapEntries);
|
||||
} catch (err) {
|
||||
@@ -568,26 +733,28 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.wizardSubmitting = true;
|
||||
const group = this.triggerDraft.group || 'DEFAULT';
|
||||
const name = this.triggerDraft.triggerName.trim();
|
||||
const request = this.wizardMode === 'edit'
|
||||
? this.schedulerService.updateSimpleTriggerConfig(command)
|
||||
: this.schedulerService.saveSimpleTriggerConfig(command);
|
||||
? this.triggerService.updateTrigger(group, name, command)
|
||||
: this.triggerService.saveTrigger(group, name, command);
|
||||
|
||||
const subscription = request.subscribe({
|
||||
next: trigger => {
|
||||
this.wizardSubmitting = false;
|
||||
this.triggerDetailsByName[this.getTriggerDetailKey(trigger.triggerKeyDTO)] = trigger as SimpleTrigger;
|
||||
this.triggerDetailsByName[this.getTriggerDetailKey(trigger.triggerKeyDTO)] = trigger;
|
||||
this.upsertTriggerKey(trigger.triggerKeyDTO);
|
||||
this.selectTrigger(trigger.triggerKeyDTO);
|
||||
this.wizardOpen = false;
|
||||
this.detailDrawerOpen = true;
|
||||
this.operationNotice = this.wizardMode === 'edit' ? 'SimpleTrigger rescheduled.' : 'SimpleTrigger created.';
|
||||
this.operationNotice = this.wizardMode === 'edit' ? 'Trigger rescheduled.' : 'Trigger created.';
|
||||
if (this.wizardMode === 'create') {
|
||||
this.resetWizard();
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.wizardSubmitting = false;
|
||||
this.wizardError = 'Unable to save the SimpleTrigger with the current backend.';
|
||||
this.wizardError = 'Unable to save the trigger.';
|
||||
}
|
||||
});
|
||||
this.subscriptions.push(subscription);
|
||||
@@ -602,7 +769,7 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
getTriggerDetail(triggerKey: TriggerKey): SimpleTrigger {
|
||||
getTriggerDetail(triggerKey: TriggerKey): Trigger {
|
||||
return triggerKey?.name ? this.triggerDetailsByName[this.getTriggerDetailKey(triggerKey)] : null;
|
||||
}
|
||||
|
||||
@@ -611,7 +778,7 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
getTriggerType(triggerKey: TriggerKey): string {
|
||||
return this.getTriggerDetail(triggerKey)?.type || 'SimpleTrigger';
|
||||
return this.getTriggerDetail(triggerKey)?.type || 'Trigger';
|
||||
}
|
||||
|
||||
getTriggerState(triggerKey: TriggerKey): string {
|
||||
@@ -779,32 +946,83 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
getWizardTitle(): string {
|
||||
return this.wizardMode === 'edit' ? 'Reschedule SimpleTrigger' : 'Create SimpleTrigger';
|
||||
return this.wizardMode === 'edit' ? 'Reschedule Trigger' : 'Create Trigger';
|
||||
}
|
||||
|
||||
getWizardCta(): string {
|
||||
return this.wizardMode === 'edit' ? 'Save Reschedule' : 'Create SimpleTrigger';
|
||||
return this.wizardMode === 'edit' ? 'Save Reschedule' : 'Create Trigger';
|
||||
}
|
||||
|
||||
canSubmitTrigger(): boolean {
|
||||
const hasTarget = this.triggerDraft.jobTargetType === 'stored'
|
||||
? !!this.triggerDraft.storedJobKey
|
||||
: !!this.triggerDraft.jobClass;
|
||||
return !!(
|
||||
this.triggerDraft.triggerName?.trim()
|
||||
return !!(this.triggerDraft.triggerName?.trim()
|
||||
&& hasTarget
|
||||
&& this.triggerDraft.misfireInstruction
|
||||
&& this.triggerDraft.repeatCount !== null
|
||||
&& this.triggerDraft.repeatCount !== undefined
|
||||
&& this.triggerDraft.repeatIntervalAmount
|
||||
&& this.triggerDraft.repeatIntervalUnit
|
||||
);
|
||||
&& this.triggerDraft.triggerType
|
||||
&& this.hasValidTriggerSchedule());
|
||||
}
|
||||
|
||||
hasValidTriggerSchedule(): boolean {
|
||||
switch (this.triggerDraft.triggerType) {
|
||||
case 'CRON': return !!this.triggerDraft.cronExpression?.trim();
|
||||
case 'DAILY_TIME_INTERVAL': return !!(this.triggerDraft.repeatIntervalAmount
|
||||
&& this.triggerDraft.repeatIntervalUnit
|
||||
&& this.triggerDraft.startTimeOfDay
|
||||
&& this.triggerDraft.endTimeOfDay);
|
||||
case 'CALENDAR_INTERVAL': return !!(this.triggerDraft.repeatIntervalAmount && this.triggerDraft.repeatIntervalUnit);
|
||||
default: return this.triggerDraft.repeatCount !== null
|
||||
&& this.triggerDraft.repeatCount !== undefined
|
||||
&& !!this.triggerDraft.repeatIntervalAmount;
|
||||
}
|
||||
}
|
||||
|
||||
canSubmitJob(): boolean {
|
||||
return !!(this.jobDraft.name?.trim() && this.jobDraft.group?.trim() && this.jobDraft.jobClass);
|
||||
}
|
||||
|
||||
canSubmitCalendar(): boolean {
|
||||
if (!this.calendarDraft.name?.trim() || !this.calendarDraft.type) {
|
||||
return false;
|
||||
}
|
||||
switch (this.calendarDraft.type) {
|
||||
case 'CRON': return !!this.calendarDraft.cronExpression?.trim();
|
||||
case 'DAILY': return !!(this.calendarDraft.rangeStartingTime && this.calendarDraft.rangeEndingTime);
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
|
||||
getCalendarRows(): QuartzCalendar[] {
|
||||
const search = this.calendarSearch?.trim().toLowerCase();
|
||||
return (this.calendars || []).filter(calendar => !search || `${calendar.name} ${calendar.type}`.toLowerCase().includes(search));
|
||||
}
|
||||
|
||||
getCalendarRuleMode(): CalendarRuleMode {
|
||||
switch (this.calendarDraft.type) {
|
||||
case 'WEEKLY': return 'weekdays';
|
||||
case 'MONTHLY': return 'monthdays';
|
||||
case 'DAILY': return 'timeRange';
|
||||
case 'CRON': return 'cron';
|
||||
default: return 'dates';
|
||||
}
|
||||
}
|
||||
|
||||
toggleCalendarWeekday(day: number) {
|
||||
this.calendarDraft.excludedDaysOfWeek = this.toggleNumberValue(this.calendarDraft.excludedDaysOfWeek, day);
|
||||
}
|
||||
|
||||
toggleCalendarMonthday(day: number) {
|
||||
this.calendarDraft.excludedDaysOfMonth = this.toggleNumberValue(this.calendarDraft.excludedDaysOfMonth, day);
|
||||
}
|
||||
|
||||
addCalendarDate() {
|
||||
this.calendarDraft.excludedDates = [...(this.calendarDraft.excludedDates || []), this.toDatetimeLocalValue(new Date())];
|
||||
}
|
||||
|
||||
removeCalendarDate(index: number) {
|
||||
this.calendarDraft.excludedDates.splice(index, 1);
|
||||
}
|
||||
|
||||
addJobDataMapEntry(entries: JobDataMapEntry[]) {
|
||||
entries.push({key: '', type: 'string', value: ''});
|
||||
}
|
||||
@@ -814,6 +1032,9 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
getFirePreview(): string[] {
|
||||
if (this.triggerDraft.triggerType === 'CRON') {
|
||||
return [`Cron expression: ${this.triggerDraft.cronExpression || '-'}`];
|
||||
}
|
||||
const start = this.fromDatetimeLocalValue(this.triggerDraft.startDate) || new Date();
|
||||
const repeatInterval = this.getRepeatIntervalMs();
|
||||
if (!repeatInterval || repeatInterval <= 0) {
|
||||
@@ -826,6 +1047,43 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
getTriggerTypeValue(trigger: Trigger): TriggerType {
|
||||
const type = trigger?.type || '';
|
||||
if (type.includes('Cron')) {
|
||||
return 'CRON';
|
||||
}
|
||||
if (type.includes('DailyTimeInterval')) {
|
||||
return 'DAILY_TIME_INTERVAL';
|
||||
}
|
||||
if (type.includes('CalendarInterval')) {
|
||||
return 'CALENDAR_INTERVAL';
|
||||
}
|
||||
return 'SIMPLE';
|
||||
}
|
||||
|
||||
selectTriggerType(triggerType: TriggerType) {
|
||||
this.triggerDraft.triggerType = triggerType;
|
||||
this.triggerDraft.misfireInstruction = this.getDefaultMisfireInstruction(triggerType);
|
||||
}
|
||||
|
||||
toggleDayOfWeek(day: number) {
|
||||
const days = new Set(this.triggerDraft.daysOfWeek || []);
|
||||
if (days.has(day)) {
|
||||
days.delete(day);
|
||||
} else {
|
||||
days.add(day);
|
||||
}
|
||||
this.triggerDraft.daysOfWeek = Array.from(days).sort((first, second) => first - second);
|
||||
}
|
||||
|
||||
isDayOfWeekSelected(day: number): boolean {
|
||||
return (this.triggerDraft.daysOfWeek || []).includes(day);
|
||||
}
|
||||
|
||||
getCalendarOptions(): string[] {
|
||||
return this.calendars.map(calendar => calendar.name);
|
||||
}
|
||||
|
||||
shortClassName(className: string): string {
|
||||
if (!className) {
|
||||
return null;
|
||||
@@ -927,6 +1185,11 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
this.scheduledJobs = [job, ...otherJobs];
|
||||
}
|
||||
|
||||
private upsertCalendar(calendar: QuartzCalendar) {
|
||||
const otherCalendars = this.calendars.filter(currentCalendar => currentCalendar.name !== calendar.name);
|
||||
this.calendars = [calendar, ...otherCalendars];
|
||||
}
|
||||
|
||||
private sameTriggerKey(first: TriggerKey, second: TriggerKey): boolean {
|
||||
return first?.name === second?.name && this.getTriggerGroup(first) === this.getTriggerGroup(second);
|
||||
}
|
||||
@@ -943,6 +1206,7 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
return {
|
||||
triggerName: '',
|
||||
group: 'DEFAULT',
|
||||
triggerType: 'SIMPLE',
|
||||
jobTargetType: this.scheduledJobs.length > 0 ? 'stored' : 'class',
|
||||
storedJobKey: this.getDefaultStoredJobKey(),
|
||||
jobClass: this.jobs[0] || '',
|
||||
@@ -952,10 +1216,35 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
repeatIntervalUnit: 'minutes',
|
||||
repeatCount: -1,
|
||||
misfireInstruction: 'MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT',
|
||||
cronExpression: '0 0/5 * * * ?',
|
||||
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
startTimeOfDay: '08:00:00',
|
||||
endTimeOfDay: '18:00:00',
|
||||
daysOfWeek: [2, 3, 4, 5, 6],
|
||||
preserveHourOfDayAcrossDaylightSavings: false,
|
||||
skipDayIfHourDoesNotExist: false,
|
||||
calendarName: '',
|
||||
jobDataMapEntries: []
|
||||
};
|
||||
}
|
||||
|
||||
private buildEmptyCalendarDraft(): CalendarDraft {
|
||||
return {
|
||||
name: '',
|
||||
type: 'WEEKLY',
|
||||
description: '',
|
||||
cronExpression: '0 0 0 ? * SAT,SUN',
|
||||
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
rangeStartingTime: '22:00:00',
|
||||
rangeEndingTime: '06:00:00',
|
||||
invertTimeRange: false,
|
||||
excludedDaysOfWeek: [1, 7],
|
||||
excludedDaysOfMonth: [],
|
||||
excludedDates: [],
|
||||
includedTime: this.toDatetimeLocalValue(new Date())
|
||||
};
|
||||
}
|
||||
|
||||
private buildEmptyJobDraft(): JobDraft {
|
||||
return {
|
||||
name: '',
|
||||
@@ -982,8 +1271,53 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
return name ? {group: group || 'DEFAULT', name} : null;
|
||||
}
|
||||
|
||||
private fromCalendarToDraft(calendar: QuartzCalendar): CalendarDraft {
|
||||
return {
|
||||
name: calendar.name,
|
||||
type: calendar.type,
|
||||
description: calendar.description || '',
|
||||
cronExpression: calendar.cronExpression || '0 0 0 ? * SAT,SUN',
|
||||
timeZone: calendar.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
rangeStartingTime: calendar.rangeStartingTime || '22:00:00',
|
||||
rangeEndingTime: calendar.rangeEndingTime || '06:00:00',
|
||||
invertTimeRange: !!calendar.invertTimeRange,
|
||||
excludedDaysOfWeek: calendar.excludedDaysOfWeek || [],
|
||||
excludedDaysOfMonth: calendar.excludedDaysOfMonth || [],
|
||||
excludedDates: (calendar.excludedDates || []).map(date => this.toDatetimeLocalValue(date)),
|
||||
includedTime: this.toDatetimeLocalValue(new Date())
|
||||
};
|
||||
}
|
||||
|
||||
private fromCalendarDraftToCommand(): QuartzCalendar {
|
||||
const calendar = new QuartzCalendar();
|
||||
calendar.name = this.calendarDraft.name.trim();
|
||||
calendar.type = this.calendarDraft.type;
|
||||
calendar.description = this.calendarDraft.description;
|
||||
calendar.cronExpression = this.calendarDraft.type === 'CRON' ? this.calendarDraft.cronExpression : null;
|
||||
calendar.timeZone = this.calendarDraft.timeZone;
|
||||
calendar.rangeStartingTime = this.calendarDraft.type === 'DAILY' ? this.calendarDraft.rangeStartingTime : null;
|
||||
calendar.rangeEndingTime = this.calendarDraft.type === 'DAILY' ? this.calendarDraft.rangeEndingTime : null;
|
||||
calendar.invertTimeRange = this.calendarDraft.type === 'DAILY' ? this.calendarDraft.invertTimeRange : null;
|
||||
calendar.excludedDaysOfWeek = this.calendarDraft.type === 'WEEKLY' ? this.calendarDraft.excludedDaysOfWeek : null;
|
||||
calendar.excludedDaysOfMonth = this.calendarDraft.type === 'MONTHLY' ? this.calendarDraft.excludedDaysOfMonth : null;
|
||||
calendar.excludedDates = ['ANNUAL', 'HOLIDAY'].includes(this.calendarDraft.type)
|
||||
? (this.calendarDraft.excludedDates || []).map(value => this.fromDatetimeLocalValue(value)).filter(Boolean)
|
||||
: null;
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private toggleNumberValue(values: number[], value: number): number[] {
|
||||
const set = new Set(values || []);
|
||||
if (set.has(value)) {
|
||||
set.delete(value);
|
||||
} else {
|
||||
set.add(value);
|
||||
}
|
||||
return Array.from(set).sort((first, second) => first - second);
|
||||
}
|
||||
|
||||
private getUniqueGroups(groups: string[]): string[] {
|
||||
return Array.from(new Set((groups || []).filter(Boolean))).sort();
|
||||
return Array.from(new Set((groups || []).filter(Boolean))).sort((first, second) => first.localeCompare(second));
|
||||
}
|
||||
|
||||
private toJobDataMapEntries(jobDataMap: {[key: string]: unknown}): JobDataMapEntry[] {
|
||||
@@ -1052,7 +1386,7 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
try {
|
||||
return JSON.parse(entry.value || 'null');
|
||||
} catch (err) {
|
||||
throw new Error(`JobDataMap key "${entry.key}" contains invalid JSON.`);
|
||||
throw new Error(`JobDataMap key "${entry.key}" contains invalid JSON: ${this.getErrorMessage(err, 'Invalid JSON')}`);
|
||||
}
|
||||
case 'null':
|
||||
return null;
|
||||
@@ -1080,6 +1414,30 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private getTriggerCommandRepeatInterval(): number {
|
||||
if (this.triggerDraft.triggerType === 'SIMPLE') {
|
||||
return this.getRepeatIntervalMs();
|
||||
}
|
||||
return Number(this.triggerDraft.repeatIntervalAmount || 0);
|
||||
}
|
||||
|
||||
private getTriggerCommandRepeatIntervalUnit(): string {
|
||||
if (this.triggerDraft.triggerType === 'SIMPLE' || this.triggerDraft.triggerType === 'CRON') {
|
||||
return null;
|
||||
}
|
||||
const unit = this.triggerDraft.repeatIntervalUnit || 'minutes';
|
||||
switch (unit) {
|
||||
case 'seconds': return 'SECOND';
|
||||
case 'minutes': return 'MINUTE';
|
||||
case 'hours': return 'HOUR';
|
||||
case 'days': return 'DAY';
|
||||
case 'weeks': return 'WEEK';
|
||||
case 'months': return 'MONTH';
|
||||
case 'years': return 'YEAR';
|
||||
default: return unit.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
private splitRepeatInterval(milliseconds: number): {amount: number; unit: string} {
|
||||
if (milliseconds && milliseconds % 86400000 === 0) {
|
||||
return {amount: milliseconds / 86400000, unit: 'days'};
|
||||
@@ -1116,7 +1474,15 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
return offsetDate.toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
private getMisfireInstructionName(misfireInstruction: number): string {
|
||||
private getMisfireInstructionName(misfireInstruction: number, triggerType: TriggerType = 'SIMPLE'): string {
|
||||
if (triggerType !== 'SIMPLE') {
|
||||
switch (misfireInstruction) {
|
||||
case 1: return 'IGNORE_MISFIRES';
|
||||
case 2: return 'FIRE_AND_PROCEED';
|
||||
case 3: return 'DO_NOTHING';
|
||||
default: return 'FIRE_AND_PROCEED';
|
||||
}
|
||||
}
|
||||
switch (misfireInstruction) {
|
||||
case 1: return 'MISFIRE_INSTRUCTION_FIRE_NOW';
|
||||
case 2: return 'MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT';
|
||||
@@ -1127,6 +1493,12 @@ export class ManagerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private getDefaultMisfireInstruction(triggerType: TriggerType): string {
|
||||
return triggerType === 'SIMPLE'
|
||||
? 'MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT'
|
||||
: 'FIRE_AND_PROCEED';
|
||||
}
|
||||
|
||||
private getPageTitle(page: ConsolePage): string {
|
||||
switch (page) {
|
||||
case 'calendars': return 'Quartz calendars';
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package it.fabioformosa.quartzmanager.api.controllers;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import it.fabioformosa.quartzmanager.api.dto.CalendarDTO;
|
||||
import it.fabioformosa.quartzmanager.api.dto.CalendarIncludedTimeDTO;
|
||||
import it.fabioformosa.quartzmanager.api.services.CalendarService;
|
||||
import jakarta.validation.Valid;
|
||||
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.PutMapping;
|
||||
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 java.text.ParseException;
|
||||
import java.util.List;
|
||||
|
||||
import static it.fabioformosa.quartzmanager.api.common.config.OpenAPIConfigConsts.QUARTZ_MANAGER_SEC_OAS_SCHEMA;
|
||||
import static it.fabioformosa.quartzmanager.api.common.config.QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH;
|
||||
|
||||
@RequestMapping(CalendarController.CALENDAR_CONTROLLER_BASE_URL)
|
||||
@SecurityRequirement(name = QUARTZ_MANAGER_SEC_OAS_SCHEMA)
|
||||
@RestController
|
||||
public class CalendarController {
|
||||
|
||||
protected static final String CALENDAR_CONTROLLER_BASE_URL = QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/calendars";
|
||||
|
||||
private final CalendarService calendarService;
|
||||
|
||||
public CalendarController(CalendarService calendarService) {
|
||||
this.calendarService = calendarService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "Get a list of calendars")
|
||||
public List<CalendarDTO> listCalendars() throws SchedulerException {
|
||||
return calendarService.fetchCalendars();
|
||||
}
|
||||
|
||||
@GetMapping("/{name}")
|
||||
@Operation(summary = "Get calendar details")
|
||||
public CalendarDTO getCalendar(@PathVariable String name) throws SchedulerException {
|
||||
return calendarService.getCalendar(name);
|
||||
}
|
||||
|
||||
@PostMapping("/{name}")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@Operation(summary = "Create a calendar")
|
||||
public CalendarDTO postCalendar(@PathVariable String name, @Valid @RequestBody CalendarDTO calendarDTO) throws SchedulerException, ParseException {
|
||||
return calendarService.addCalendar(name, calendarDTO);
|
||||
}
|
||||
|
||||
@PutMapping("/{name}")
|
||||
@Operation(summary = "Update a calendar")
|
||||
public CalendarDTO putCalendar(@PathVariable String name, @Valid @RequestBody CalendarDTO calendarDTO) throws SchedulerException, ParseException {
|
||||
return calendarService.updateCalendar(name, calendarDTO);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{name}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@Operation(summary = "Delete a calendar")
|
||||
public void deleteCalendar(@PathVariable String name) throws SchedulerException {
|
||||
calendarService.deleteCalendar(name);
|
||||
}
|
||||
|
||||
@PostMapping("/{name}/included-time-test")
|
||||
@Operation(summary = "Test if a time is included by a calendar")
|
||||
public CalendarIncludedTimeDTO testIncludedTime(@PathVariable String name, @Valid @RequestBody CalendarIncludedTimeDTO input) throws SchedulerException {
|
||||
return calendarService.testIncludedTime(name, input);
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,10 @@ 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.dto.TriggerInputDTO;
|
||||
import it.fabioformosa.quartzmanager.api.exceptions.TriggerNotFoundException;
|
||||
import it.fabioformosa.quartzmanager.api.services.TriggerService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.quartz.SchedulerException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -17,10 +19,13 @@ 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.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.List;
|
||||
|
||||
import static it.fabioformosa.quartzmanager.api.common.config.OpenAPIConfigConsts.QUARTZ_MANAGER_SEC_OAS_SCHEMA;
|
||||
@@ -63,6 +68,25 @@ public class TriggerController {
|
||||
return triggerService.getTrigger(group, name);
|
||||
}
|
||||
|
||||
@PostMapping("/{group}/{name}")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@Operation(summary = "Schedule a new trigger")
|
||||
public TriggerDTO postTrigger(@PathVariable String group, @PathVariable String name, @Valid @RequestBody TriggerInputDTO triggerInputDTO) throws SchedulerException, ClassNotFoundException, ParseException {
|
||||
log.info("TRIGGER - CREATING a trigger {} {}", name, triggerInputDTO);
|
||||
TriggerDTO newTriggerDTO = triggerService.scheduleTrigger(group, name, triggerInputDTO);
|
||||
log.info("TRIGGER - CREATED a trigger {}", newTriggerDTO);
|
||||
return newTriggerDTO;
|
||||
}
|
||||
|
||||
@PutMapping("/{group}/{name}")
|
||||
@Operation(summary = "Reschedule a trigger")
|
||||
public TriggerDTO rescheduleTrigger(@PathVariable String group, @PathVariable String name, @Valid @RequestBody TriggerInputDTO triggerInputDTO) throws SchedulerException, TriggerNotFoundException, ParseException {
|
||||
log.info("TRIGGER - RESCHEDULING the trigger {} {}", name, triggerInputDTO);
|
||||
TriggerDTO triggerDTO = triggerService.rescheduleTrigger(group, name, triggerInputDTO);
|
||||
log.info("TRIGGER - RESCHEDULED the trigger {}", triggerDTO);
|
||||
return triggerDTO;
|
||||
}
|
||||
|
||||
@PostMapping("/{group}/{name}/pause")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@Operation(summary = "Pause a trigger")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package it.fabioformosa.quartzmanager.api.controllers.advices;
|
||||
|
||||
import it.fabioformosa.quartzmanager.api.exceptions.ExceptionResponse;
|
||||
import it.fabioformosa.quartzmanager.api.exceptions.CalendarNotFoundException;
|
||||
import it.fabioformosa.quartzmanager.api.exceptions.JobNotFoundException;
|
||||
import it.fabioformosa.quartzmanager.api.exceptions.ResourceConflictException;
|
||||
import it.fabioformosa.quartzmanager.api.exceptions.TriggerNotFoundException;
|
||||
@@ -37,6 +38,13 @@ public class ExceptionHandlingController {
|
||||
return ExceptionResponse.builder().errorCode(HttpStatus.NOT_FOUND.toString()).errorMessage(ex.getMessage()).build();
|
||||
}
|
||||
|
||||
@ExceptionHandler(CalendarNotFoundException.class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
@ResponseBody
|
||||
public ExceptionResponse calendarNotFound(CalendarNotFoundException ex){
|
||||
return ExceptionResponse.builder().errorCode(HttpStatus.NOT_FOUND.toString()).errorMessage(ex.getMessage()).build();
|
||||
}
|
||||
|
||||
@ExceptionHandler(UnsupportedTriggerTypeException.class)
|
||||
public ResponseEntity<ExceptionResponse> unsupportedTriggerType(UnsupportedTriggerTypeException ex) {
|
||||
ExceptionResponse response = ExceptionResponse.builder()
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package it.fabioformosa.quartzmanager.api.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class CalendarDTO {
|
||||
private String name;
|
||||
|
||||
@NotNull
|
||||
private CalendarType type;
|
||||
|
||||
private String description;
|
||||
private String cronExpression;
|
||||
private String timeZone;
|
||||
private String rangeStartingTime;
|
||||
private String rangeEndingTime;
|
||||
private Boolean invertTimeRange;
|
||||
private Set<Integer> excludedDaysOfWeek;
|
||||
private Set<Integer> excludedDaysOfMonth;
|
||||
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||
private List<Date> excludedDates;
|
||||
|
||||
private List<TriggerKeyDTO> triggerKeys;
|
||||
|
||||
public List<Date> getExcludedDatesOrEmpty() {
|
||||
return excludedDates == null ? Collections.emptyList() : excludedDates;
|
||||
}
|
||||
|
||||
public Set<Integer> getExcludedDaysOfWeekOrEmpty() {
|
||||
return excludedDaysOfWeek == null ? Collections.emptySet() : excludedDaysOfWeek;
|
||||
}
|
||||
|
||||
public Set<Integer> getExcludedDaysOfMonthOrEmpty() {
|
||||
return excludedDaysOfMonth == null ? Collections.emptySet() : excludedDaysOfMonth;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package it.fabioformosa.quartzmanager.api.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class CalendarIncludedTimeDTO {
|
||||
@NotNull
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||
private Date time;
|
||||
|
||||
private Boolean included;
|
||||
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||
private Date nextIncludedTime;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package it.fabioformosa.quartzmanager.api.dto;
|
||||
|
||||
public enum CalendarType {
|
||||
ANNUAL,
|
||||
CRON,
|
||||
DAILY,
|
||||
HOLIDAY,
|
||||
MONTHLY,
|
||||
WEEKLY
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import lombok.experimental.SuperBuilder;
|
||||
@SuperBuilder
|
||||
public class SimpleTriggerDTO extends TriggerDTO{
|
||||
|
||||
private int repeatCount;
|
||||
private long repeatInterval;
|
||||
private int timesTriggered;
|
||||
private Integer repeatCount;
|
||||
private Long repeatInterval;
|
||||
private Integer timesTriggered;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package it.fabioformosa.quartzmanager.api.dto;
|
||||
|
||||
import it.fabioformosa.quartzmanager.api.validators.ValidTriggerRepetition;
|
||||
import it.fabioformosa.quartzmanager.api.validators.JobTargetDTO;
|
||||
import it.fabioformosa.quartzmanager.api.validators.ValidJobTarget;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import jakarta.annotation.Nullable;
|
||||
@@ -8,13 +10,14 @@ import jakarta.validation.constraints.Positive;
|
||||
import java.util.Map;
|
||||
|
||||
@ValidTriggerRepetition
|
||||
@ValidJobTarget
|
||||
@SuperBuilder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@ToString(callSuper = true)
|
||||
public class SimpleTriggerInputDTO extends TriggerCommandDTO implements TriggerRepetitionDTO {
|
||||
public class SimpleTriggerInputDTO extends TriggerCommandDTO implements TriggerRepetitionDTO, JobTargetDTO {
|
||||
private Integer repeatCount;
|
||||
|
||||
@Positive
|
||||
|
||||
@@ -7,6 +7,7 @@ import lombok.experimental.SuperBuilder;
|
||||
import org.quartz.JobDataMap;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@@ -29,4 +30,14 @@ public class TriggerDTO {
|
||||
private JobDetailDTO jobDetailDTO;
|
||||
private boolean mayFireAgain;
|
||||
private JobDataMap jobDataMap;
|
||||
private String cronExpression;
|
||||
private String timeZone;
|
||||
private Long repeatInterval;
|
||||
private Integer repeatCount;
|
||||
private String repeatIntervalUnit;
|
||||
private String startTimeOfDay;
|
||||
private String endTimeOfDay;
|
||||
private Set<Integer> daysOfWeek;
|
||||
private Boolean preserveHourOfDayAcrossDaylightSavings;
|
||||
private Boolean skipDayIfHourDoesNotExist;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package it.fabioformosa.quartzmanager.api.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import it.fabioformosa.quartzmanager.api.validators.JobTargetDTO;
|
||||
import it.fabioformosa.quartzmanager.api.validators.ValidJobTarget;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Builder
|
||||
@ValidJobTarget
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class TriggerInputDTO implements JobTargetDTO {
|
||||
@NotNull
|
||||
private TriggerType triggerType;
|
||||
|
||||
@Nullable
|
||||
private String jobClass;
|
||||
|
||||
@Nullable
|
||||
private JobKeyDTO jobKey;
|
||||
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||
private Date startDate;
|
||||
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||
private Date endDate;
|
||||
|
||||
private String description;
|
||||
private Integer priority;
|
||||
private String calendarName;
|
||||
private String misfireInstruction;
|
||||
|
||||
@Nullable
|
||||
private Map<String, ?> jobDataMap;
|
||||
|
||||
private Integer repeatCount;
|
||||
|
||||
@Positive
|
||||
private Long repeatInterval;
|
||||
|
||||
private String repeatIntervalUnit;
|
||||
private String cronExpression;
|
||||
private String timeZone;
|
||||
private String startTimeOfDay;
|
||||
private String endTimeOfDay;
|
||||
private Set<Integer> daysOfWeek;
|
||||
private Boolean preserveHourOfDayAcrossDaylightSavings;
|
||||
private Boolean skipDayIfHourDoesNotExist;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package it.fabioformosa.quartzmanager.api.dto;
|
||||
|
||||
public enum TriggerType {
|
||||
SIMPLE,
|
||||
CRON,
|
||||
DAILY_TIME_INTERVAL,
|
||||
CALENDAR_INTERVAL
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package it.fabioformosa.quartzmanager.api.exceptions;
|
||||
|
||||
public class CalendarNotFoundException extends RuntimeException {
|
||||
public CalendarNotFoundException(String name) {
|
||||
super("Calendar " + name + " not found!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package it.fabioformosa.quartzmanager.api.services;
|
||||
|
||||
import it.fabioformosa.quartzmanager.api.dto.CalendarDTO;
|
||||
import it.fabioformosa.quartzmanager.api.dto.CalendarIncludedTimeDTO;
|
||||
import it.fabioformosa.quartzmanager.api.dto.CalendarType;
|
||||
import it.fabioformosa.quartzmanager.api.dto.TriggerKeyDTO;
|
||||
import it.fabioformosa.quartzmanager.api.exceptions.CalendarNotFoundException;
|
||||
import it.fabioformosa.quartzmanager.api.exceptions.ResourceConflictException;
|
||||
import org.quartz.Calendar;
|
||||
import org.quartz.Scheduler;
|
||||
import org.quartz.SchedulerException;
|
||||
import org.quartz.Trigger;
|
||||
import org.quartz.TriggerKey;
|
||||
import org.quartz.impl.calendar.AnnualCalendar;
|
||||
import org.quartz.impl.calendar.CronCalendar;
|
||||
import org.quartz.impl.calendar.DailyCalendar;
|
||||
import org.quartz.impl.calendar.HolidayCalendar;
|
||||
import org.quartz.impl.calendar.MonthlyCalendar;
|
||||
import org.quartz.impl.calendar.WeeklyCalendar;
|
||||
import org.quartz.impl.matchers.GroupMatcher;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
import java.util.TreeSet;
|
||||
|
||||
@Service
|
||||
public class CalendarService {
|
||||
|
||||
private final Scheduler scheduler;
|
||||
|
||||
public CalendarService(@Qualifier("quartzManagerScheduler") Scheduler scheduler) {
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
public List<CalendarDTO> fetchCalendars() throws SchedulerException {
|
||||
return scheduler.getCalendarNames().stream()
|
||||
.map(this::getCalendarUnchecked)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public CalendarDTO getCalendar(String name) throws SchedulerException {
|
||||
Calendar calendar = scheduler.getCalendar(name);
|
||||
if (calendar == null)
|
||||
throw new CalendarNotFoundException(name);
|
||||
return toDTO(name, calendar);
|
||||
}
|
||||
|
||||
public CalendarDTO addCalendar(String name, CalendarDTO calendarDTO) throws SchedulerException, ParseException {
|
||||
if (scheduler.getCalendar(name) != null)
|
||||
throw new ResourceConflictException("Calendar " + name + " already exists");
|
||||
Calendar calendar = buildCalendar(calendarDTO);
|
||||
scheduler.addCalendar(name, calendar, false, false);
|
||||
return toDTO(name, calendar);
|
||||
}
|
||||
|
||||
public CalendarDTO updateCalendar(String name, CalendarDTO calendarDTO) throws SchedulerException, ParseException {
|
||||
if (scheduler.getCalendar(name) == null)
|
||||
throw new CalendarNotFoundException(name);
|
||||
Calendar calendar = buildCalendar(calendarDTO);
|
||||
scheduler.addCalendar(name, calendar, true, true);
|
||||
return toDTO(name, calendar);
|
||||
}
|
||||
|
||||
public void deleteCalendar(String name) throws SchedulerException {
|
||||
if (!scheduler.deleteCalendar(name))
|
||||
throw new CalendarNotFoundException(name);
|
||||
}
|
||||
|
||||
public CalendarIncludedTimeDTO testIncludedTime(String name, CalendarIncludedTimeDTO input) throws SchedulerException {
|
||||
Calendar calendar = scheduler.getCalendar(name);
|
||||
if (calendar == null)
|
||||
throw new CalendarNotFoundException(name);
|
||||
long timestamp = input.getTime().getTime();
|
||||
return CalendarIncludedTimeDTO.builder()
|
||||
.time(input.getTime())
|
||||
.included(calendar.isTimeIncluded(timestamp))
|
||||
.nextIncludedTime(new Date(calendar.getNextIncludedTime(timestamp)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private CalendarDTO getCalendarUnchecked(String name) {
|
||||
try {
|
||||
return getCalendar(name);
|
||||
} catch (SchedulerException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Calendar buildCalendar(CalendarDTO calendarDTO) throws ParseException {
|
||||
Calendar calendar = switch (calendarDTO.getType()) {
|
||||
case ANNUAL -> buildAnnualCalendar(calendarDTO);
|
||||
case CRON -> buildCronCalendar(calendarDTO);
|
||||
case DAILY -> buildDailyCalendar(calendarDTO);
|
||||
case HOLIDAY -> buildHolidayCalendar(calendarDTO);
|
||||
case MONTHLY -> buildMonthlyCalendar(calendarDTO);
|
||||
case WEEKLY -> buildWeeklyCalendar(calendarDTO);
|
||||
};
|
||||
calendar.setDescription(calendarDTO.getDescription());
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private AnnualCalendar buildAnnualCalendar(CalendarDTO calendarDTO) {
|
||||
AnnualCalendar calendar = new AnnualCalendar();
|
||||
for (Date excludedDate : calendarDTO.getExcludedDatesOrEmpty()) {
|
||||
java.util.Calendar excludedDay = java.util.Calendar.getInstance();
|
||||
excludedDay.setTime(excludedDate);
|
||||
calendar.setDayExcluded(excludedDay, true);
|
||||
}
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private CronCalendar buildCronCalendar(CalendarDTO calendarDTO) throws ParseException {
|
||||
CronCalendar calendar = new CronCalendar(calendarDTO.getCronExpression());
|
||||
if (calendarDTO.getTimeZone() != null && !calendarDTO.getTimeZone().isBlank())
|
||||
calendar.setTimeZone(TimeZone.getTimeZone(calendarDTO.getTimeZone()));
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private DailyCalendar buildDailyCalendar(CalendarDTO calendarDTO) {
|
||||
DailyCalendar calendar = new DailyCalendar(calendarDTO.getRangeStartingTime(), calendarDTO.getRangeEndingTime());
|
||||
calendar.setInvertTimeRange(Boolean.TRUE.equals(calendarDTO.getInvertTimeRange()));
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private HolidayCalendar buildHolidayCalendar(CalendarDTO calendarDTO) {
|
||||
HolidayCalendar calendar = new HolidayCalendar();
|
||||
for (Date excludedDate : calendarDTO.getExcludedDatesOrEmpty())
|
||||
calendar.addExcludedDate(excludedDate);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private MonthlyCalendar buildMonthlyCalendar(CalendarDTO calendarDTO) {
|
||||
MonthlyCalendar calendar = new MonthlyCalendar();
|
||||
for (Integer day : calendarDTO.getExcludedDaysOfMonthOrEmpty())
|
||||
calendar.setDayExcluded(day, true);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private WeeklyCalendar buildWeeklyCalendar(CalendarDTO calendarDTO) {
|
||||
WeeklyCalendar calendar = new WeeklyCalendar();
|
||||
for (Integer day : calendarDTO.getExcludedDaysOfWeekOrEmpty())
|
||||
calendar.setDayExcluded(day, true);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private CalendarDTO toDTO(String name, Calendar calendar) throws SchedulerException {
|
||||
CalendarDTO calendarDTO = CalendarDTO.builder()
|
||||
.name(name)
|
||||
.description(calendar.getDescription())
|
||||
.triggerKeys(findTriggerKeys(name))
|
||||
.build();
|
||||
|
||||
if (calendar instanceof AnnualCalendar annualCalendar)
|
||||
enrichAnnualCalendar(calendarDTO, annualCalendar);
|
||||
else if (calendar instanceof CronCalendar cronCalendar)
|
||||
enrichCronCalendar(calendarDTO, cronCalendar);
|
||||
else if (calendar instanceof DailyCalendar dailyCalendar)
|
||||
enrichDailyCalendar(calendarDTO, dailyCalendar);
|
||||
else if (calendar instanceof HolidayCalendar holidayCalendar)
|
||||
enrichHolidayCalendar(calendarDTO, holidayCalendar);
|
||||
else if (calendar instanceof MonthlyCalendar monthlyCalendar)
|
||||
enrichMonthlyCalendar(calendarDTO, monthlyCalendar);
|
||||
else if (calendar instanceof WeeklyCalendar weeklyCalendar)
|
||||
enrichWeeklyCalendar(calendarDTO, weeklyCalendar);
|
||||
return calendarDTO;
|
||||
}
|
||||
|
||||
private void enrichAnnualCalendar(CalendarDTO calendarDTO, AnnualCalendar calendar) {
|
||||
calendarDTO.setType(CalendarType.ANNUAL);
|
||||
calendarDTO.setExcludedDates(calendar.getDaysExcluded().stream().map(java.util.Calendar::getTime).toList());
|
||||
}
|
||||
|
||||
private void enrichCronCalendar(CalendarDTO calendarDTO, CronCalendar calendar) {
|
||||
calendarDTO.setType(CalendarType.CRON);
|
||||
calendarDTO.setCronExpression(calendar.getCronExpression().getCronExpression());
|
||||
calendarDTO.setTimeZone(calendar.getTimeZone().getID());
|
||||
}
|
||||
|
||||
private void enrichDailyCalendar(CalendarDTO calendarDTO, DailyCalendar calendar) {
|
||||
calendarDTO.setType(CalendarType.DAILY);
|
||||
long now = System.currentTimeMillis();
|
||||
calendarDTO.setRangeStartingTime(formatTime(calendar.getTimeRangeStartingTimeInMillis(now)));
|
||||
calendarDTO.setRangeEndingTime(formatTime(calendar.getTimeRangeEndingTimeInMillis(now)));
|
||||
calendarDTO.setInvertTimeRange(calendar.getInvertTimeRange());
|
||||
}
|
||||
|
||||
private void enrichHolidayCalendar(CalendarDTO calendarDTO, HolidayCalendar calendar) {
|
||||
calendarDTO.setType(CalendarType.HOLIDAY);
|
||||
calendarDTO.setExcludedDates(new ArrayList<>(calendar.getExcludedDates()));
|
||||
}
|
||||
|
||||
private void enrichMonthlyCalendar(CalendarDTO calendarDTO, MonthlyCalendar calendar) {
|
||||
calendarDTO.setType(CalendarType.MONTHLY);
|
||||
Set<Integer> excludedDays = new TreeSet<>();
|
||||
for (int day = 1; day <= 31; day++) {
|
||||
if (calendar.isDayExcluded(day))
|
||||
excludedDays.add(day);
|
||||
}
|
||||
calendarDTO.setExcludedDaysOfMonth(excludedDays);
|
||||
}
|
||||
|
||||
private void enrichWeeklyCalendar(CalendarDTO calendarDTO, WeeklyCalendar calendar) {
|
||||
calendarDTO.setType(CalendarType.WEEKLY);
|
||||
Set<Integer> excludedDays = new TreeSet<>();
|
||||
for (int day = java.util.Calendar.SUNDAY; day <= java.util.Calendar.SATURDAY; day++) {
|
||||
if (calendar.isDayExcluded(day))
|
||||
excludedDays.add(day);
|
||||
}
|
||||
calendarDTO.setExcludedDaysOfWeek(excludedDays);
|
||||
}
|
||||
|
||||
private List<TriggerKeyDTO> findTriggerKeys(String calendarName) throws SchedulerException {
|
||||
List<TriggerKeyDTO> triggerKeys = new ArrayList<>();
|
||||
for (TriggerKey triggerKey : scheduler.getTriggerKeys(GroupMatcher.anyTriggerGroup())) {
|
||||
Trigger trigger = scheduler.getTrigger(triggerKey);
|
||||
if (trigger != null && calendarName.equals(trigger.getCalendarName()))
|
||||
triggerKeys.add(TriggerKeyDTO.builder().name(triggerKey.getName()).group(triggerKey.getGroup()).build());
|
||||
}
|
||||
return triggerKeys;
|
||||
}
|
||||
|
||||
private String formatTime(long timeInMillis) {
|
||||
java.util.Calendar calendar = java.util.Calendar.getInstance();
|
||||
calendar.setTimeInMillis(timeInMillis);
|
||||
return String.format("%02d:%02d:%02d", calendar.get(java.util.Calendar.HOUR_OF_DAY), calendar.get(java.util.Calendar.MINUTE), calendar.get(java.util.Calendar.SECOND));
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,51 @@
|
||||
package it.fabioformosa.quartzmanager.api.services;
|
||||
|
||||
import it.fabioformosa.quartzmanager.api.dto.JobKeyDTO;
|
||||
import it.fabioformosa.quartzmanager.api.dto.MisfireInstruction;
|
||||
import it.fabioformosa.quartzmanager.api.dto.TriggerDTO;
|
||||
import it.fabioformosa.quartzmanager.api.dto.TriggerInputDTO;
|
||||
import it.fabioformosa.quartzmanager.api.dto.TriggerKeyDTO;
|
||||
import it.fabioformosa.quartzmanager.api.dto.TriggerType;
|
||||
import it.fabioformosa.quartzmanager.api.exceptions.ResourceConflictException;
|
||||
import it.fabioformosa.quartzmanager.api.exceptions.TriggerNotFoundException;
|
||||
import org.quartz.CalendarIntervalScheduleBuilder;
|
||||
import org.quartz.CalendarIntervalTrigger;
|
||||
import org.quartz.CronScheduleBuilder;
|
||||
import org.quartz.CronTrigger;
|
||||
import org.quartz.DailyTimeIntervalScheduleBuilder;
|
||||
import org.quartz.DailyTimeIntervalTrigger;
|
||||
import org.quartz.DateBuilder;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobBuilder;
|
||||
import org.quartz.JobDataMap;
|
||||
import org.quartz.JobDetail;
|
||||
import org.quartz.JobKey;
|
||||
import org.quartz.ScheduleBuilder;
|
||||
import org.quartz.Scheduler;
|
||||
import org.quartz.SchedulerException;
|
||||
import org.quartz.SimpleScheduleBuilder;
|
||||
import org.quartz.SimpleTrigger;
|
||||
import org.quartz.TimeOfDay;
|
||||
import org.quartz.Trigger;
|
||||
import org.quartz.TriggerBuilder;
|
||||
import org.quartz.TriggerKey;
|
||||
import org.quartz.impl.matchers.GroupMatcher;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
|
||||
@Service
|
||||
public class TriggerService {
|
||||
|
||||
private Scheduler scheduler;
|
||||
private ConversionService conversionService;
|
||||
private static final int DEFAULT_PRIORITY = Trigger.DEFAULT_PRIORITY;
|
||||
|
||||
private final Scheduler scheduler;
|
||||
private final ConversionService conversionService;
|
||||
|
||||
public TriggerService(@Qualifier("quartzManagerScheduler") Scheduler scheduler, ConversionService conversionService) {
|
||||
this.scheduler = scheduler;
|
||||
@@ -38,11 +64,47 @@ public class TriggerService {
|
||||
Trigger trigger = scheduler.getTrigger(triggerKey);
|
||||
if (trigger == null)
|
||||
throw new TriggerNotFoundException(group, name);
|
||||
TriggerDTO triggerDTO = conversionService.convert(trigger, TriggerDTO.class);
|
||||
TriggerDTO triggerDTO = convertTrigger(trigger);
|
||||
triggerDTO.setState(scheduler.getTriggerState(triggerKey).name());
|
||||
return triggerDTO;
|
||||
}
|
||||
|
||||
public TriggerDTO scheduleTrigger(String group, String name, TriggerInputDTO triggerInputDTO) throws SchedulerException, ClassNotFoundException, ParseException {
|
||||
TriggerKey triggerKey = TriggerKey.triggerKey(name, group);
|
||||
if (scheduler.checkExists(triggerKey))
|
||||
throw new ResourceConflictException("Trigger " + triggerKey + " already exists");
|
||||
|
||||
Trigger newTrigger = buildTrigger(group, name, triggerInputDTO);
|
||||
JobKey jobKey = getJobKey(triggerInputDTO);
|
||||
if (jobKey != null) {
|
||||
if (!scheduler.checkExists(jobKey))
|
||||
throw new ResourceConflictException("Job " + jobKey + " does not exist");
|
||||
scheduler.scheduleJob(newTrigger.getTriggerBuilder().forJob(jobKey).build());
|
||||
}
|
||||
else {
|
||||
JobDetail jobDetail = JobBuilder.newJob()
|
||||
.ofType(Class.forName(triggerInputDTO.getJobClass()).asSubclass(Job.class))
|
||||
.storeDurably(false)
|
||||
.build();
|
||||
scheduler.scheduleJob(jobDetail, newTrigger);
|
||||
}
|
||||
|
||||
return convertTrigger(newTrigger);
|
||||
}
|
||||
|
||||
public TriggerDTO rescheduleTrigger(String group, String name, TriggerInputDTO triggerInputDTO) throws SchedulerException, TriggerNotFoundException, ParseException {
|
||||
TriggerKey triggerKey = TriggerKey.triggerKey(name, group);
|
||||
Trigger existingTrigger = scheduler.getTrigger(triggerKey);
|
||||
if (existingTrigger == null)
|
||||
throw new TriggerNotFoundException(group, name);
|
||||
|
||||
Trigger newTrigger = buildTrigger(group, name, triggerInputDTO).getTriggerBuilder()
|
||||
.forJob(existingTrigger.getJobKey())
|
||||
.build();
|
||||
scheduler.rescheduleJob(triggerKey, newTrigger);
|
||||
return convertTrigger(newTrigger);
|
||||
}
|
||||
|
||||
public void pauseTrigger(String group, String name) throws SchedulerException, TriggerNotFoundException {
|
||||
TriggerKey triggerKey = requireTrigger(group, name);
|
||||
scheduler.pauseTrigger(triggerKey);
|
||||
@@ -65,4 +127,183 @@ public class TriggerService {
|
||||
return triggerKey;
|
||||
}
|
||||
|
||||
private Trigger buildTrigger(String group, String name, TriggerInputDTO triggerInputDTO) throws ParseException {
|
||||
TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger()
|
||||
.withIdentity(name, group)
|
||||
.withPriority(triggerInputDTO.getPriority() == null ? DEFAULT_PRIORITY : triggerInputDTO.getPriority());
|
||||
|
||||
if (triggerInputDTO.getStartDate() != null)
|
||||
triggerBuilder.startAt(triggerInputDTO.getStartDate());
|
||||
if (triggerInputDTO.getEndDate() != null)
|
||||
triggerBuilder.endAt(triggerInputDTO.getEndDate());
|
||||
if (triggerInputDTO.getDescription() != null)
|
||||
triggerBuilder.withDescription(triggerInputDTO.getDescription());
|
||||
if (triggerInputDTO.getCalendarName() != null && !triggerInputDTO.getCalendarName().isBlank())
|
||||
triggerBuilder.modifiedByCalendar(triggerInputDTO.getCalendarName());
|
||||
if (triggerInputDTO.getJobDataMap() != null)
|
||||
triggerBuilder.usingJobData(new JobDataMap(triggerInputDTO.getJobDataMap()));
|
||||
|
||||
return triggerBuilder.withSchedule(buildSchedule(triggerInputDTO)).build();
|
||||
}
|
||||
|
||||
private ScheduleBuilder<?> buildSchedule(TriggerInputDTO triggerInputDTO) throws ParseException {
|
||||
TriggerType triggerType = triggerInputDTO.getTriggerType() == null ? TriggerType.SIMPLE : triggerInputDTO.getTriggerType();
|
||||
return switch (triggerType) {
|
||||
case SIMPLE -> buildSimpleSchedule(triggerInputDTO);
|
||||
case CRON -> buildCronSchedule(triggerInputDTO);
|
||||
case DAILY_TIME_INTERVAL -> buildDailyTimeIntervalSchedule(triggerInputDTO);
|
||||
case CALENDAR_INTERVAL -> buildCalendarIntervalSchedule(triggerInputDTO);
|
||||
};
|
||||
}
|
||||
|
||||
private SimpleScheduleBuilder buildSimpleSchedule(TriggerInputDTO triggerInputDTO) {
|
||||
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule();
|
||||
if (triggerInputDTO.getRepeatInterval() != null)
|
||||
scheduleBuilder.withIntervalInMilliseconds(triggerInputDTO.getRepeatInterval());
|
||||
if (triggerInputDTO.getRepeatCount() != null)
|
||||
scheduleBuilder.withRepeatCount(triggerInputDTO.getRepeatCount());
|
||||
|
||||
MisfireInstruction misfireInstruction = parseSimpleMisfireInstruction(triggerInputDTO.getMisfireInstruction());
|
||||
switch (misfireInstruction) {
|
||||
case MISFIRE_INSTRUCTION_FIRE_NOW -> scheduleBuilder.withMisfireHandlingInstructionFireNow();
|
||||
case MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT -> scheduleBuilder.withMisfireHandlingInstructionNowWithExistingCount();
|
||||
case MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT -> scheduleBuilder.withMisfireHandlingInstructionNowWithRemainingCount();
|
||||
case MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT -> scheduleBuilder.withMisfireHandlingInstructionNextWithRemainingCount();
|
||||
case MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT -> scheduleBuilder.withMisfireHandlingInstructionNextWithExistingCount();
|
||||
}
|
||||
return scheduleBuilder;
|
||||
}
|
||||
|
||||
private CronScheduleBuilder buildCronSchedule(TriggerInputDTO triggerInputDTO) throws ParseException {
|
||||
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronScheduleNonvalidatedExpression(triggerInputDTO.getCronExpression());
|
||||
if (triggerInputDTO.getTimeZone() != null && !triggerInputDTO.getTimeZone().isBlank())
|
||||
scheduleBuilder.inTimeZone(TimeZone.getTimeZone(triggerInputDTO.getTimeZone()));
|
||||
return applyCronMisfireInstruction(scheduleBuilder, triggerInputDTO.getMisfireInstruction());
|
||||
}
|
||||
|
||||
private DailyTimeIntervalScheduleBuilder buildDailyTimeIntervalSchedule(TriggerInputDTO triggerInputDTO) {
|
||||
DailyTimeIntervalScheduleBuilder scheduleBuilder = DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()
|
||||
.withInterval(Math.toIntExact(triggerInputDTO.getRepeatInterval()), parseIntervalUnit(triggerInputDTO.getRepeatIntervalUnit(), DateBuilder.IntervalUnit.MINUTE));
|
||||
if (triggerInputDTO.getStartTimeOfDay() != null && !triggerInputDTO.getStartTimeOfDay().isBlank())
|
||||
scheduleBuilder.startingDailyAt(parseTimeOfDay(triggerInputDTO.getStartTimeOfDay()));
|
||||
if (triggerInputDTO.getEndTimeOfDay() != null && !triggerInputDTO.getEndTimeOfDay().isBlank())
|
||||
scheduleBuilder.endingDailyAt(parseTimeOfDay(triggerInputDTO.getEndTimeOfDay()));
|
||||
if (triggerInputDTO.getDaysOfWeek() != null && !triggerInputDTO.getDaysOfWeek().isEmpty())
|
||||
scheduleBuilder.onDaysOfTheWeek(triggerInputDTO.getDaysOfWeek());
|
||||
return applyDailyMisfireInstruction(scheduleBuilder, triggerInputDTO.getMisfireInstruction());
|
||||
}
|
||||
|
||||
private CalendarIntervalScheduleBuilder buildCalendarIntervalSchedule(TriggerInputDTO triggerInputDTO) {
|
||||
CalendarIntervalScheduleBuilder scheduleBuilder = CalendarIntervalScheduleBuilder.calendarIntervalSchedule()
|
||||
.withInterval(Math.toIntExact(triggerInputDTO.getRepeatInterval()), parseIntervalUnit(triggerInputDTO.getRepeatIntervalUnit(), DateBuilder.IntervalUnit.DAY));
|
||||
if (Boolean.TRUE.equals(triggerInputDTO.getPreserveHourOfDayAcrossDaylightSavings()))
|
||||
scheduleBuilder.preserveHourOfDayAcrossDaylightSavings(true);
|
||||
if (Boolean.TRUE.equals(triggerInputDTO.getSkipDayIfHourDoesNotExist()))
|
||||
scheduleBuilder.skipDayIfHourDoesNotExist(true);
|
||||
if (triggerInputDTO.getTimeZone() != null && !triggerInputDTO.getTimeZone().isBlank())
|
||||
scheduleBuilder.inTimeZone(TimeZone.getTimeZone(triggerInputDTO.getTimeZone()));
|
||||
return applyCalendarIntervalMisfireInstruction(scheduleBuilder, triggerInputDTO.getMisfireInstruction());
|
||||
}
|
||||
|
||||
private JobKey getJobKey(TriggerInputDTO triggerInputDTO) {
|
||||
JobKeyDTO jobKeyDTO = triggerInputDTO.getJobKey();
|
||||
if (jobKeyDTO == null)
|
||||
return null;
|
||||
return JobKey.jobKey(jobKeyDTO.getName(), jobKeyDTO.getGroup());
|
||||
}
|
||||
|
||||
private TriggerDTO convertTrigger(Trigger trigger) {
|
||||
TriggerDTO triggerDTO = conversionService.convert(trigger, TriggerDTO.class);
|
||||
if (triggerDTO == null)
|
||||
triggerDTO = new TriggerDTO();
|
||||
if (trigger instanceof SimpleTrigger simpleTrigger)
|
||||
enrichSimpleTrigger(triggerDTO, simpleTrigger);
|
||||
else if (trigger instanceof CronTrigger cronTrigger)
|
||||
enrichCronTrigger(triggerDTO, cronTrigger);
|
||||
else if (trigger instanceof DailyTimeIntervalTrigger dailyTimeIntervalTrigger)
|
||||
enrichDailyTimeIntervalTrigger(triggerDTO, dailyTimeIntervalTrigger);
|
||||
else if (trigger instanceof CalendarIntervalTrigger calendarIntervalTrigger)
|
||||
enrichCalendarIntervalTrigger(triggerDTO, calendarIntervalTrigger);
|
||||
return triggerDTO;
|
||||
}
|
||||
|
||||
private void enrichSimpleTrigger(TriggerDTO triggerDTO, SimpleTrigger simpleTrigger) {
|
||||
triggerDTO.setRepeatCount(simpleTrigger.getRepeatCount());
|
||||
triggerDTO.setRepeatInterval(simpleTrigger.getRepeatInterval());
|
||||
}
|
||||
|
||||
private void enrichCronTrigger(TriggerDTO triggerDTO, CronTrigger cronTrigger) {
|
||||
triggerDTO.setCronExpression(cronTrigger.getCronExpression());
|
||||
triggerDTO.setTimeZone(cronTrigger.getTimeZone().getID());
|
||||
}
|
||||
|
||||
private void enrichDailyTimeIntervalTrigger(TriggerDTO triggerDTO, DailyTimeIntervalTrigger dailyTrigger) {
|
||||
triggerDTO.setRepeatCount(dailyTrigger.getRepeatCount());
|
||||
triggerDTO.setRepeatInterval((long) dailyTrigger.getRepeatInterval());
|
||||
triggerDTO.setRepeatIntervalUnit(dailyTrigger.getRepeatIntervalUnit().name());
|
||||
triggerDTO.setStartTimeOfDay(formatTimeOfDay(dailyTrigger.getStartTimeOfDay()));
|
||||
triggerDTO.setEndTimeOfDay(formatTimeOfDay(dailyTrigger.getEndTimeOfDay()));
|
||||
triggerDTO.setDaysOfWeek(dailyTrigger.getDaysOfWeek());
|
||||
}
|
||||
|
||||
private void enrichCalendarIntervalTrigger(TriggerDTO triggerDTO, CalendarIntervalTrigger calendarTrigger) {
|
||||
triggerDTO.setRepeatInterval((long) calendarTrigger.getRepeatInterval());
|
||||
triggerDTO.setRepeatIntervalUnit(calendarTrigger.getRepeatIntervalUnit().name());
|
||||
triggerDTO.setPreserveHourOfDayAcrossDaylightSavings(calendarTrigger.isPreserveHourOfDayAcrossDaylightSavings());
|
||||
triggerDTO.setSkipDayIfHourDoesNotExist(calendarTrigger.isSkipDayIfHourDoesNotExist());
|
||||
triggerDTO.setTimeZone(calendarTrigger.getTimeZone().getID());
|
||||
}
|
||||
|
||||
private MisfireInstruction parseSimpleMisfireInstruction(String misfireInstruction) {
|
||||
if (misfireInstruction == null || misfireInstruction.isBlank())
|
||||
return MisfireInstruction.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT;
|
||||
return MisfireInstruction.valueOf(misfireInstruction);
|
||||
}
|
||||
|
||||
private CronScheduleBuilder applyCronMisfireInstruction(CronScheduleBuilder scheduleBuilder, String misfireInstruction) {
|
||||
return switch (normalizeMisfireInstruction(misfireInstruction)) {
|
||||
case "DO_NOTHING" -> scheduleBuilder.withMisfireHandlingInstructionDoNothing();
|
||||
case "IGNORE_MISFIRES" -> scheduleBuilder.withMisfireHandlingInstructionIgnoreMisfires();
|
||||
default -> scheduleBuilder.withMisfireHandlingInstructionFireAndProceed();
|
||||
};
|
||||
}
|
||||
|
||||
private DailyTimeIntervalScheduleBuilder applyDailyMisfireInstruction(DailyTimeIntervalScheduleBuilder scheduleBuilder, String misfireInstruction) {
|
||||
return switch (normalizeMisfireInstruction(misfireInstruction)) {
|
||||
case "DO_NOTHING" -> scheduleBuilder.withMisfireHandlingInstructionDoNothing();
|
||||
case "IGNORE_MISFIRES" -> scheduleBuilder.withMisfireHandlingInstructionIgnoreMisfires();
|
||||
default -> scheduleBuilder.withMisfireHandlingInstructionFireAndProceed();
|
||||
};
|
||||
}
|
||||
|
||||
private CalendarIntervalScheduleBuilder applyCalendarIntervalMisfireInstruction(CalendarIntervalScheduleBuilder scheduleBuilder, String misfireInstruction) {
|
||||
return switch (normalizeMisfireInstruction(misfireInstruction)) {
|
||||
case "DO_NOTHING" -> scheduleBuilder.withMisfireHandlingInstructionDoNothing();
|
||||
case "IGNORE_MISFIRES" -> scheduleBuilder.withMisfireHandlingInstructionIgnoreMisfires();
|
||||
default -> scheduleBuilder.withMisfireHandlingInstructionFireAndProceed();
|
||||
};
|
||||
}
|
||||
|
||||
private String normalizeMisfireInstruction(String misfireInstruction) {
|
||||
return misfireInstruction == null || misfireInstruction.isBlank() ? "FIRE_AND_PROCEED" : misfireInstruction;
|
||||
}
|
||||
|
||||
private DateBuilder.IntervalUnit parseIntervalUnit(String intervalUnit, DateBuilder.IntervalUnit defaultUnit) {
|
||||
return intervalUnit == null || intervalUnit.isBlank() ? defaultUnit : DateBuilder.IntervalUnit.valueOf(intervalUnit);
|
||||
}
|
||||
|
||||
private TimeOfDay parseTimeOfDay(String timeOfDay) {
|
||||
String[] parts = timeOfDay.split(":");
|
||||
int hour = Integer.parseInt(parts[0]);
|
||||
int minute = parts.length > 1 ? Integer.parseInt(parts[1]) : 0;
|
||||
int second = parts.length > 2 ? Integer.parseInt(parts[2]) : 0;
|
||||
return TimeOfDay.hourMinuteAndSecondOfDay(hour, minute, second);
|
||||
}
|
||||
|
||||
private String formatTimeOfDay(TimeOfDay timeOfDay) {
|
||||
if (timeOfDay == null)
|
||||
return null;
|
||||
return String.format("%02d:%02d:%02d", timeOfDay.getHour(), timeOfDay.getMinute(), timeOfDay.getSecond());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package it.fabioformosa.quartzmanager.api.validators;
|
||||
|
||||
import it.fabioformosa.quartzmanager.api.dto.JobKeyDTO;
|
||||
|
||||
public interface JobTargetDTO {
|
||||
String getJobClass();
|
||||
JobKeyDTO getJobKey();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package it.fabioformosa.quartzmanager.api.validators;
|
||||
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Constraint(validatedBy = ValidJobTargetValidator.class)
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ValidJobTarget {
|
||||
String message() default "Either jobClass or jobKey must be set";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package it.fabioformosa.quartzmanager.api.validators;
|
||||
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
|
||||
public class ValidJobTargetValidator implements ConstraintValidator<ValidJobTarget, JobTargetDTO> {
|
||||
@Override
|
||||
public boolean isValid(JobTargetDTO jobTargetDTO, ConstraintValidatorContext constraintValidatorContext) {
|
||||
if (jobTargetDTO == null)
|
||||
return true;
|
||||
boolean hasJobClass = jobTargetDTO.getJobClass() != null && !jobTargetDTO.getJobClass().isBlank();
|
||||
boolean hasJobKey = jobTargetDTO.getJobKey() != null && jobTargetDTO.getJobKey().getName() != null && !jobTargetDTO.getJobKey().getName().isBlank();
|
||||
return hasJobClass || hasJobKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
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.CalendarDTO;
|
||||
import it.fabioformosa.quartzmanager.api.dto.CalendarIncludedTimeDTO;
|
||||
import it.fabioformosa.quartzmanager.api.dto.CalendarType;
|
||||
import it.fabioformosa.quartzmanager.api.services.CalendarService;
|
||||
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.ContextConfiguration;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
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;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
|
||||
@ContextConfiguration(classes = {QuartManagerApplicationTests.class})
|
||||
@WebMvcTest(controllers = CalendarController.class, properties = {
|
||||
"quartz-manager.jobClassPackages=it.fabioformosa.quartzmanager.jobs"
|
||||
})
|
||||
class CalendarControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockitoBean
|
||||
private CalendarService calendarService;
|
||||
|
||||
@AfterEach
|
||||
void cleanUp(){
|
||||
Mockito.reset(calendarService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenListCalendarsIsCalled_thenCalendarsAreReturned() throws Exception {
|
||||
List<CalendarDTO> calendars = List.of(CalendarDTO.builder().name("weekends").type(CalendarType.WEEKLY).excludedDaysOfWeek(Set.of(1, 7)).build());
|
||||
Mockito.when(calendarService.fetchCalendars()).thenReturn(calendars);
|
||||
|
||||
mockMvc.perform(get(CalendarController.CALENDAR_CONTROLLER_BASE_URL).contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(calendars)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenGetCalendarIsCalled_thenCalendarIsReturned() throws Exception {
|
||||
CalendarDTO calendarDTO = CalendarDTO.builder().name("cron").type(CalendarType.CRON).cronExpression("0 0 0 ? * SAT,SUN").build();
|
||||
Mockito.when(calendarService.getCalendar("cron")).thenReturn(calendarDTO);
|
||||
|
||||
mockMvc.perform(get(CalendarController.CALENDAR_CONTROLLER_BASE_URL + "/cron").contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(calendarDTO)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenACalendarDTO_whenPosted_thenCalendarIsCreated() throws Exception {
|
||||
CalendarDTO calendarDTO = CalendarDTO.builder().name("holidays").type(CalendarType.HOLIDAY).excludedDates(List.of(new Date())).build();
|
||||
Mockito.when(calendarService.addCalendar(Mockito.eq("holidays"), any())).thenReturn(calendarDTO);
|
||||
|
||||
mockMvc.perform(post(CalendarController.CALENDAR_CONTROLLER_BASE_URL + "/holidays")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(TestUtils.toJson(calendarDTO)))
|
||||
.andExpect(MockMvcResultMatchers.status().isCreated())
|
||||
.andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(calendarDTO)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenACalendarDTO_whenPut_thenCalendarIsUpdated() throws Exception {
|
||||
CalendarDTO calendarDTO = CalendarDTO.builder().name("month-end").type(CalendarType.MONTHLY).excludedDaysOfMonth(Set.of(31)).build();
|
||||
Mockito.when(calendarService.updateCalendar("month-end", calendarDTO)).thenReturn(calendarDTO);
|
||||
|
||||
mockMvc.perform(put(CalendarController.CALENDAR_CONTROLLER_BASE_URL + "/month-end")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(TestUtils.toJson(calendarDTO)))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(calendarDTO)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenDeleteCalendarIsCalled_thenNoContentIsReturned() throws Exception {
|
||||
mockMvc.perform(delete(CalendarController.CALENDAR_CONTROLLER_BASE_URL + "/weekends").contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(MockMvcResultMatchers.status().isNoContent());
|
||||
|
||||
Mockito.verify(calendarService).deleteCalendar("weekends");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenIncludedTimeIsTested_thenResultIsReturned() throws Exception {
|
||||
CalendarIncludedTimeDTO input = CalendarIncludedTimeDTO.builder().time(new Date()).build();
|
||||
CalendarIncludedTimeDTO result = CalendarIncludedTimeDTO.builder().time(input.getTime()).included(true).nextIncludedTime(input.getTime()).build();
|
||||
Mockito.when(calendarService.testIncludedTime(Mockito.eq("weekends"), any())).thenReturn(result);
|
||||
|
||||
mockMvc.perform(post(CalendarController.CALENDAR_CONTROLLER_BASE_URL + "/weekends/included-time-test")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(TestUtils.toJson(input)))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(result)));
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ 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.TriggerInputDTO;
|
||||
import it.fabioformosa.quartzmanager.api.dto.TriggerKeyDTO;
|
||||
import it.fabioformosa.quartzmanager.api.dto.TriggerType;
|
||||
import it.fabioformosa.quartzmanager.api.services.TriggerService;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -21,6 +23,7 @@ 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;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
|
||||
@ContextConfiguration(classes = {QuartManagerApplicationTests.class})
|
||||
@WebMvcTest(controllers = TriggerController.class, properties = {
|
||||
@@ -63,6 +66,49 @@ class TriggerControllerTest {
|
||||
.andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(triggerDTO)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenATriggerInputDTO_whenPosted_thenTriggerIsCreated() throws Exception {
|
||||
TriggerInputDTO triggerInputDTO = TriggerInputDTO.builder()
|
||||
.triggerType(TriggerType.CRON)
|
||||
.jobClass("it.fabioformosa.quartzmanager.api.jobs.SampleJob")
|
||||
.cronExpression("0 0/5 * * * ?")
|
||||
.misfireInstruction("FIRE_AND_PROCEED")
|
||||
.build();
|
||||
TriggerDTO triggerDTO = TriggerDTO.builder()
|
||||
.triggerKeyDTO(TriggerKeyDTO.builder().name("cronTrigger").group("DEFAULT").build())
|
||||
.type("CronTriggerImpl")
|
||||
.build();
|
||||
Mockito.when(triggerService.scheduleTrigger("DEFAULT", "cronTrigger", triggerInputDTO)).thenReturn(triggerDTO);
|
||||
|
||||
mockMvc.perform(post(TriggerController.TRIGGER_CONTROLLER_BASE_URL + "/DEFAULT/cronTrigger")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(TestUtils.toJson(triggerInputDTO)))
|
||||
.andExpect(MockMvcResultMatchers.status().isCreated())
|
||||
.andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(triggerDTO)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenATriggerInputDTO_whenPut_thenTriggerIsRescheduled() throws Exception {
|
||||
TriggerInputDTO triggerInputDTO = TriggerInputDTO.builder()
|
||||
.triggerType(TriggerType.CALENDAR_INTERVAL)
|
||||
.jobClass("it.fabioformosa.quartzmanager.api.jobs.SampleJob")
|
||||
.repeatInterval(2L)
|
||||
.repeatIntervalUnit("DAY")
|
||||
.misfireInstruction("DO_NOTHING")
|
||||
.build();
|
||||
TriggerDTO triggerDTO = TriggerDTO.builder()
|
||||
.triggerKeyDTO(TriggerKeyDTO.builder().name("calendarTrigger").group("DEFAULT").build())
|
||||
.type("CalendarIntervalTriggerImpl")
|
||||
.build();
|
||||
Mockito.when(triggerService.rescheduleTrigger("DEFAULT", "calendarTrigger", triggerInputDTO)).thenReturn(triggerDTO);
|
||||
|
||||
mockMvc.perform(put(TriggerController.TRIGGER_CONTROLLER_BASE_URL + "/DEFAULT/calendarTrigger")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(TestUtils.toJson(triggerInputDTO)))
|
||||
.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))
|
||||
|
||||
@@ -72,7 +72,7 @@ class SimpleTriggerServiceTest {
|
||||
|
||||
SimpleTriggerDTO expectedTriggerDTO = SimpleTriggerDTO.builder()
|
||||
.startTime(triggerInputDTO.getStartDate())
|
||||
.repeatInterval(1000)
|
||||
.repeatInterval(1000L)
|
||||
.repeatCount(10)
|
||||
.mayFireAgain(true)
|
||||
.finalFireTime(triggerInputDTO.getEndDate())
|
||||
@@ -110,7 +110,7 @@ class SimpleTriggerServiceTest {
|
||||
|
||||
SimpleTriggerDTO expectedTriggerDTO = SimpleTriggerDTO.builder()
|
||||
.startTime(triggerInputDTO.getStartDate())
|
||||
.repeatInterval(1000)
|
||||
.repeatInterval(1000L)
|
||||
.repeatCount(10)
|
||||
.mayFireAgain(true)
|
||||
.finalFireTime(triggerInputDTO.getEndDate())
|
||||
|
||||
Reference in New Issue
Block a user