#52 added the jobClass selection

This commit is contained in:
Fabio Formosa
2022-09-13 23:38:31 +02:00
parent a644dd6052
commit d9f9ee96af
24 changed files with 226 additions and 88 deletions

View File

@@ -18,6 +18,7 @@ import {MatIconModule} from '@angular/material/icon';
import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card';
import {MatDatepickerModule} from '@angular/material/datepicker';
import {MatSelectModule} from '@angular/material/select';
import {MatListModule} from '@angular/material/list';
import {MatSidenavModule} from '@angular/material/sidenav';
@@ -50,6 +51,7 @@ import {
AuthService,
UserService,
SchedulerService,
JobService,
ConfigService,
ProgressWebsocketService,
LogsWebsocketService,
@@ -138,6 +140,7 @@ export function jwtOptionsFactory(apiService: ApiService) {
MatChipsModule,
MatIconModule,
MatInputModule,
MatSelectModule,
MatToolbarModule,
MatCardModule,
MatListModule,
@@ -164,6 +167,7 @@ export function jwtOptionsFactory(apiService: ApiService) {
GuestGuard,
AdminGuard,
SchedulerService,
JobService,
TriggerService,
ProgressWebsocketService,
LogsWebsocketService,

View File

@@ -20,10 +20,11 @@
<div>
<mat-form-field [appearance]="enabledTriggerForm ? 'standard': 'none'">
<mat-label>Job Class</mat-label>
<input id="jobClass"
[readonly]="!enabledTriggerForm"
matInput placeholder="Job Class Name" name="jobClass"
[(ngModel)]="simpleTriggerForm.jobClass">
<mat-select name="jobClass" [(ngModel)]="simpleTriggerForm.jobClass" [disabled]="!enabledTriggerForm">
<mat-option *ngFor="let job of jobs" [value]="job">
{{job}}
</mat-option>
</mat-select>
</mat-form-field>
</div>

View File

@@ -18,6 +18,7 @@ import {Trigger} from '../../model/trigger.model';
import {JobDetail} from '../../model/jobDetail.model';
import {SimpleTriggerForm} from '../../model/simple-trigger.form';
import {SimpleTrigger} from '../../model/simple-trigger.model';
import JobService from '../../services/job.service';
describe('SimpleTriggerConfig', () => {
@@ -33,7 +34,7 @@ describe('SimpleTriggerConfig', () => {
MatNativeDateModule,
MatCardModule, MatIconModule, HttpClientTestingModule, RouterTestingModule],
declarations: [SimpleTriggerConfigComponent],
providers: [SchedulerService, ApiService, ConfigService],
providers: [SchedulerService, ApiService, ConfigService, JobService],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

View File

@@ -1,5 +1,5 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {SchedulerService} from '../../services';
import {JobService, SchedulerService} from '../../services';
import {Scheduler} from '../../model/scheduler.model';
import {SimpleTriggerCommand} from '../../model/simple-trigger.command';
import {SimpleTrigger} from '../../model/simple-trigger.model';
@@ -27,16 +27,24 @@ export class SimpleTriggerConfigComponent implements OnInit {
private selectedTriggerKey: TriggerKey;
private jobs: Array<String>;
enabledTriggerForm = false;
@Output()
onNewTrigger = new EventEmitter<SimpleTrigger>();
constructor(
private schedulerService: SchedulerService
private schedulerService: SchedulerService,
private jobService: JobService
) { }
ngOnInit() {
this.fetchJobs();
}
private fetchJobs() {
this.jobService.fetchJobs().subscribe(jobs => this.jobs = jobs);
}
openTriggerForm() {
@@ -109,6 +117,7 @@ export class SimpleTriggerConfigComponent implements OnInit {
private _fromFormToCommand = (simpleTriggerForm: SimpleTriggerForm): SimpleTriggerCommand => {
const simpleTriggerCommand = new SimpleTriggerCommand();
simpleTriggerCommand.triggerName = simpleTriggerForm.triggerName;
simpleTriggerCommand.jobClass = simpleTriggerForm.jobClass;
simpleTriggerCommand.repeatCount = simpleTriggerForm.repeatCount;
simpleTriggerCommand.repeatInterval = simpleTriggerForm.repeatInterval;
simpleTriggerCommand.startDate = simpleTriggerForm.startDate?.toDate();

View File

@@ -1,5 +1,6 @@
export class SimpleTriggerCommand {
triggerName: string;
jobClass: string;
startDate: Date;
endDate: Date;
repeatCount: number;

View File

@@ -1,24 +1,28 @@
import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
import {Injectable} from '@angular/core';
import {environment} from '../../environments/environment';
const WEBJAR_PATH = '/quartz-manager-ui/';
export function getHtmlBaseUrl(){
const baseUrl = getBaseUrl() || '/';
return environment.production ? getBaseUrl() + WEBJAR_PATH: '/';
}
export const CONTEXT_PATH = '/quartz-manager';
export function getBaseUrl(){
if(environment.production){
let contextPath: string = window.location.pathname.split('/')[1] || '';
if(contextPath && ('/' + contextPath + '/') === WEBJAR_PATH)
return '';
if(contextPath)
contextPath = '/' + contextPath;
return contextPath;
}
return '';
export function getHtmlBaseUrl() {
const baseUrl = getBaseUrl() || '/';
return environment.production ? getBaseUrl() + WEBJAR_PATH : '/';
}
export function getBaseUrl() {
if (environment.production) {
let contextPath: string = window.location.pathname.split('/')[1] || '';
if (contextPath && ('/' + contextPath + '/') === WEBJAR_PATH) {
return '';
}
if (contextPath) {
contextPath = '/' + contextPath;
}
return contextPath;
}
return '';
}
@Injectable()
@@ -45,35 +49,35 @@ export class ConfigService {
private _signup_url = this._api_url + '/signup';
get reset_credentials_url(): string {
return this._reset_credentials_url;
return this._reset_credentials_url;
}
get refresh_token_url(): string {
return this._refresh_token_url;
return this._refresh_token_url;
}
get whoami_url(): string {
return this._whoami_url;
return this._whoami_url;
}
get users_url(): string {
return this._users_url;
return this._users_url;
}
get login_url(): string {
return this._login_url;
return this._login_url;
}
get logout_url(): string {
return this._logout_url;
return this._logout_url;
}
get change_password_url(): string {
return this._change_password_url;
return this._change_password_url;
}
get signup_url():string {
return this._signup_url;
get signup_url(): string {
return this._signup_url;
}
}

View File

@@ -7,4 +7,6 @@ export * from './websocket.service';
export * from './progress.websocket.service';
export * from './logs.websocket.service';
export * from './trigger.service'
export * from './job.service'

View File

@@ -0,0 +1,18 @@
import {Injectable} from '@angular/core';
import {ApiService} from './api.service';
import {CONTEXT_PATH, getBaseUrl} from './config.service';
import {Observable} from 'rxjs';
@Injectable()
export class JobService {
constructor(
private apiService: ApiService
) {
}
fetchJobs = (): Observable<string[]> => {
return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/jobs`)
}
}

View File

@@ -1,18 +1,15 @@
import { Injectable } from '@angular/core';
import { getBaseUrl } from '.';
import { ApiService } from './api.service';
import {Injectable} from '@angular/core';
import {CONTEXT_PATH, getBaseUrl} from '.';
import {ApiService} from './api.service';
import {Trigger} from '../model/trigger.model';
import {Observable} from 'rxjs';
import {SimpleTriggerCommand} from '../model/simple-trigger.command';
import {SchedulerConfig} from '../model/schedulerConfig.model';
import {Scheduler} from '../model/scheduler.model';
const CONTEXT_PATH = '/quartz-manager';
@Injectable()
export class SchedulerService {
constructor(
private apiService: ApiService
) { }

View File

@@ -97,6 +97,11 @@
<artifactId>javax.el</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
<!-- QUARTZ -->
<dependency>

View File

@@ -1,10 +1,8 @@
package it.fabioformosa.quartzmanager.controllers;
import org.springframework.beans.factory.annotation.Value;
public class AbstractTriggerController {
@Value("${quartz-manager.jobClass}")
protected String jobClassname;
// @Value("${quartz-manager.jobClass}")
// protected String jobClassname;
}

View File

@@ -1,21 +1,26 @@
package it.fabioformosa.quartzmanager.controllers;
import it.fabioformosa.quartzmanager.services.JobService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@RequestMapping("/quartz-manager/jobs")
@RestController
public class JobController extends AbstractTriggerController {
private JobService jobService;
public JobController(JobService jobService) {
this.jobService = jobService;
}
@GetMapping
public List<String> listJobs(){
List jobClasses = new ArrayList();
jobClasses.add(jobClassname);
return jobClasses;
return jobService.getJobClasses().stream().map(Class::getName).collect(Collectors.toList());
}
}

View File

@@ -62,7 +62,7 @@ public class SimpleTriggerController extends AbstractTriggerController {
.triggerName(name)
.simpleTriggerInputDTO(simpleTriggerInputDTO)
.build();
SimpleTriggerDTO newTriggerDTO = simpleSchedulerService.scheduleSimpleTrigger(jobClassname, simpleTriggerCommandDTO);
SimpleTriggerDTO newTriggerDTO = simpleSchedulerService.scheduleSimpleTrigger(simpleTriggerCommandDTO);
log.info("SIMPLE TRIGGER - CREATED a SimpleTrigger {}", newTriggerDTO);
return newTriggerDTO;
}

View File

@@ -13,7 +13,6 @@ import it.fabioformosa.quartzmanager.services.LegacySchedulerService;
import it.fabioformosa.quartzmanager.services.TriggerService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.SchedulerException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@@ -46,23 +45,23 @@ public class TriggerController extends AbstractTriggerController {
return schedulerService.getLegacyTriggerByName(name);
}
@Deprecated
@PostMapping("/{name}")
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "Create a new trigger")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "Created the new trigger",
content = { @Content(mediaType = "application/json",
schema = @Schema(implementation = TriggerDTO.class)) }),
@ApiResponse(responseCode = "400", description = "Invalid config supplied",
content = @Content)
})
public TriggerDTO postTrigger(@PathVariable String name, @Valid @RequestBody SchedulerConfigParam config) throws SchedulerException, ClassNotFoundException {
log.info("TRIGGER - CREATING a trigger {} {}", name, config);
TriggerDTO newTriggerDTO = schedulerService.scheduleNewTrigger(name, jobClassname, config);
log.info("TRIGGER - CREATED a trigger {}", newTriggerDTO);
return newTriggerDTO;
}
// @Deprecated
// @PostMapping("/{name}")
// @ResponseStatus(HttpStatus.CREATED)
// @Operation(summary = "Create a new trigger")
// @ApiResponses(value = {
// @ApiResponse(responseCode = "201", description = "Created the new trigger",
// content = { @Content(mediaType = "application/json",
// schema = @Schema(implementation = TriggerDTO.class)) }),
// @ApiResponse(responseCode = "400", description = "Invalid config supplied",
// content = @Content)
// })
// public TriggerDTO postTrigger(@PathVariable String name, @Valid @RequestBody SchedulerConfigParam config) throws SchedulerException, ClassNotFoundException {
// log.info("TRIGGER - CREATING a trigger {} {}", name, config);
// TriggerDTO newTriggerDTO = schedulerService.scheduleNewTrigger(name, config);
// log.info("TRIGGER - CREATED a trigger {}", newTriggerDTO);
// return newTriggerDTO;
// }
@PutMapping("/{name}")
@Operation(summary = "Reschedule the trigger")

View File

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;
import lombok.experimental.SuperBuilder;
import javax.validation.constraints.NotNull;
import java.util.Date;
@SuperBuilder
@@ -14,6 +15,9 @@ import java.util.Date;
@Data
public class TriggerCommandDTO {
@NotNull
private String jobClass;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private Date startDate;

View File

@@ -0,0 +1,43 @@
package it.fabioformosa.quartzmanager.services;
import it.fabioformosa.quartzmanager.jobs.AbstractLoggingJob;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.reflections.Reflections;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class JobService {
@Getter
private List<Class<? extends AbstractLoggingJob>> jobClasses = new ArrayList<>();
private List<String> jobClassPackages = new ArrayList<>();
public JobService(@Value("${quartz-manager.jobClassPackages}") String jobClassPackages) {
List<String> splitPackages = Arrays.stream(Optional.of(jobClassPackages).map(str -> str.split(",")).get())
.map(String::trim)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
if (splitPackages.size() > 0)
this.jobClassPackages.addAll(splitPackages);
}
@PostConstruct
public void initJobClassList() {
List<Class<? extends AbstractLoggingJob>> foundJobClasses = jobClassPackages.stream().flatMap(jobClassPackage -> findJobClassesInPackage(jobClassPackage).stream()).collect(Collectors.toList());
if (foundJobClasses.size() > 0)
this.jobClasses.addAll(foundJobClasses);
}
private static Set<Class<? extends AbstractLoggingJob>> findJobClassesInPackage(String packageStr) {
Reflections reflections = new Reflections(packageStr);
return reflections.getSubTypesOf(AbstractLoggingJob.class);
}
}

View File

@@ -20,8 +20,8 @@ public class SimpleTriggerSchedulerService extends AbstractSchedulerService {
return conversionService.convert(trigger, SimpleTriggerDTO.class);
}
public SimpleTriggerDTO scheduleSimpleTrigger(String jobClassname, SimpleTriggerCommandDTO triggerCommandDTO) throws SchedulerException, ClassNotFoundException {
Class<? extends Job> jobClass = (Class<? extends Job>) Class.forName(jobClassname);
public SimpleTriggerDTO scheduleSimpleTrigger(SimpleTriggerCommandDTO triggerCommandDTO) throws SchedulerException, ClassNotFoundException {
Class<? extends Job> jobClass = (Class<? extends Job>) Class.forName(triggerCommandDTO.getSimpleTriggerInputDTO().getJobClass());
JobDetail jobDetail = JobBuilder.newJob()
.ofType(jobClass)
.storeDurably(false)

View File

@@ -29,7 +29,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
@ContextConfiguration(classes = {QuartManagerApplicationTests.class})
@WebMvcTest(controllers = SimpleTriggerController.class, properties = {
"quartz-manager.jobClass=it.fabioformosa.quartzmanager.jobs.SampleJob"
"quartz-manager.jobClassPackages=it.fabioformosa.quartzmanager.jobs"
})
class SimpleTriggerControllerTest {
@@ -66,7 +66,7 @@ class SimpleTriggerControllerTest {
void givenASimpleTriggerCommandDTO_whenPosted_thenANewSimpleTriggerIsCreated() throws Exception {
SimpleTriggerInputDTO simpleTriggerInputDTO = buildSimpleTriggerCommandDTO();
SimpleTriggerDTO expectedSimpleTriggerDTO = TriggerUtils.getSimpleTriggerInstance("mytrigger", simpleTriggerInputDTO);
Mockito.when(simpleTriggerSchedulerService.scheduleSimpleTrigger(any(), any())).thenReturn(expectedSimpleTriggerDTO);
Mockito.when(simpleTriggerSchedulerService.scheduleSimpleTrigger(any())).thenReturn(expectedSimpleTriggerDTO);
mockMvc.perform(
post(SimpleTriggerController.SIMPLE_TRIGGER_CONTROLLER_BASE_URL + "/mytrigger")
.contentType(MediaType.APPLICATION_JSON)
@@ -79,6 +79,7 @@ class SimpleTriggerControllerTest {
private SimpleTriggerInputDTO buildSimpleTriggerCommandDTO() {
return SimpleTriggerInputDTO.builder()
.jobClass("it.fabioformosa.quartzmanager.jobs.SampleJob")
.startDate(new Date())
.repeatCount(20)
.repeatInterval(20000L)

View File

@@ -20,12 +20,11 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import static org.mockito.ArgumentMatchers.any;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
@ContextConfiguration(classes = {QuartManagerApplicationTests.class})
@WebMvcTest(controllers = TriggerController.class, properties = {
"quartz-manager.jobClass=it.fabioformosa.quartzmanager.jobs.SampleJob"
"quartz-manager.jobClassPackages=it.fabioformosa.quartzmanager.jobs"
})
class TriggerControllerTest {
@@ -40,21 +39,6 @@ class TriggerControllerTest {
Mockito.reset(schedulerService);
}
@Test
void givenASchedulerConfigParam_whenPosted_thenANewTriggerIsCreated() throws Exception {
SchedulerConfigParam configParamToPost = buildSimpleSchedulerConfigParam();
TriggerDTO expectedTriggerDTO = TriggerUtils.getTriggerInstance("mytrigger");
Mockito.when(schedulerService.scheduleNewTrigger(any(), any(), any())).thenReturn(expectedTriggerDTO);
mockMvc.perform(
post(TriggerController.TRIGGER_CONTROLLER_BASE_URL + "/mytrigger")
.contentType(MediaType.APPLICATION_JSON)
.content(TestUtils.toJson(configParamToPost))
)
.andExpect(MockMvcResultMatchers.status().isCreated())
.andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(expectedTriggerDTO)))
;
}
@ParameterizedTest
@ArgumentsSource(InvalidSchedulerConfigParamProvider.class)
void givenAnInvalidSchedulerConfigParam_whenRequestedANewTrigger_thenAnErrorIsReturned(SchedulerConfigParam invalidSchedulerConfigParam) throws Exception {

View File

@@ -0,0 +1,45 @@
package it.fabioformosa.quartzmanager.services;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
class JobServiceTest {
@Test
void givenTwoJobClassesInTwoPackages_whenTheJobServiceIsCalled_shouldReturnTwoJobClasses(){
JobService jobService = new JobService("it.fabioformosa.quartzmanager.jobs, it.fabioformosa.samplepackage");
jobService.initJobClassList();
Assertions.assertThat(jobService).isNotNull();
Assertions.assertThat(jobService.getJobClasses().size()).isEqualTo(2);
}
@ParameterizedTest
@ValueSource(strings = {
"it.fabioformosa.quartzmanager.jobs",
"it.fabioformosa.quartzmanager.jobs,",
",it.fabioformosa.quartzmanager.jobs"
})
void givenOnePackage_whenTheJobServiceIsCalled_shouldReturnOneJobClasses(String packageStr){
JobService jobService = new JobService(packageStr);
jobService.initJobClassList();
Assertions.assertThat(jobService).isNotNull();
Assertions.assertThat(jobService.getJobClasses().size()).isEqualTo(1);
}
@ParameterizedTest
@ValueSource(strings = {
"",
",",
", "
})
void givenNoPackages_whenTheJobServiceIsCalled_shouldReturnNoJobClasses(String packageStr){
JobService jobService = new JobService(packageStr);
jobService.initJobClassList();
Assertions.assertThat(jobService).isNotNull();
Assertions.assertThat(jobService.getJobClasses()).isEmpty();
}
}

View File

@@ -48,6 +48,7 @@ class SimpleTriggerSchedulerServiceTest {
@Test
void givenASimpleTriggerCommandDTO_whenASimpleTriggerIsScheduled_thenATriggerDTOIsReturned() throws SchedulerException, ClassNotFoundException {
SimpleTriggerInputDTO triggerInputDTO = SimpleTriggerInputDTO.builder()
.jobClass("it.fabioformosa.quartzmanager.jobs.SampleJob")
.startDate(new Date())
.repeatInterval(5000L).repeatCount(5)
.endDate(DateUtils.getHoursFromNow(1))
@@ -73,7 +74,7 @@ class SimpleTriggerSchedulerServiceTest {
.triggerName(simpleTriggerName)
.simpleTriggerInputDTO(triggerInputDTO)
.build();
SimpleTriggerDTO simpleTrigger = simpleSchedulerService.scheduleSimpleTrigger("it.fabioformosa.quartzmanager.jobs.SampleJob", simpleTriggerCommandDTO);
SimpleTriggerDTO simpleTrigger = simpleSchedulerService.scheduleSimpleTrigger(simpleTriggerCommandDTO);
Assertions.assertThat(simpleTrigger).isEqualTo(expectedTriggerDTO);
}

View File

@@ -0,0 +1,15 @@
package it.fabioformosa.samplepackage;
import it.fabioformosa.quartzmanager.jobs.AbstractLoggingJob;
import it.fabioformosa.quartzmanager.jobs.entities.LogRecord;
import it.fabioformosa.quartzmanager.jobs.entities.LogRecord.LogType;
import org.quartz.JobExecutionContext;
public class SampleExtraJob extends AbstractLoggingJob {
@Override
public LogRecord doIt(JobExecutionContext jobExecutionContext) {
return new LogRecord(LogType.INFO, "Hello!");
}
}

View File

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

View File

@@ -52,6 +52,7 @@ quartz-manager:
enabled: true
cookie: AUTH-TOKEN
jobClass: it.fabioformosa.quartzmanager.jobs.myjobs.SampleJob
jobClassPackages: it.fabioformosa.quartzmanager.jobs
accounts:
in-memory:
enabled: true