Merge pull request #10364 from vpavic

* pr/10364:
  Polish "Add Quartz actuator endpoint"
  Add Quartz actuator endpoint

Closes gh-10364
pull/30708/head
Stephane Nicoll 4 years ago
commit a07c8e6e18

@ -86,6 +86,7 @@ dependencies {
optional("org.mongodb:mongodb-driver-reactivestreams") optional("org.mongodb:mongodb-driver-reactivestreams")
optional("org.mongodb:mongodb-driver-sync") optional("org.mongodb:mongodb-driver-sync")
optional("org.neo4j.driver:neo4j-java-driver") optional("org.neo4j.driver:neo4j-java-driver")
optional("org.quartz-scheduler:quartz")
optional("org.springframework:spring-jdbc") optional("org.springframework:spring-jdbc")
optional("org.springframework:spring-jms") optional("org.springframework:spring-jms")
optional("org.springframework:spring-messaging") optional("org.springframework:spring-messaging")

@ -0,0 +1,247 @@
[[quartz]]
= Quartz (`quartz`)
The `quartz` endpoint provides information about jobs and triggers that are managed by the Quartz Scheduler.
[[quartz-report]]
== Retrieving Registered Groups
Jobs and triggers are managed in groups.
To retrieve the list of registered job and trigger groups, make a `GET` request to `/actuator/quartz`, as shown in the following curl-based example:
include::{snippets}/quartz/report/curl-request.adoc[]
The resulting response is similar to the following:
include::{snippets}/quartz/report/http-response.adoc[]
[[quartz-report-response-structure]]
=== Response Structure
The response contains the groups names for registered jobs and triggers.
The following table describes the structure of the response:
[cols="3,1,3"]
include::{snippets}/quartz/report/response-fields.adoc[]
[[quartz-job-groups]]
== Retrieving Registered Job Names
To retrieve the list of registered job names, make a `GET` request to `/actuator/quartz/jobs`, as shown in the following curl-based example:
include::{snippets}/quartz/jobs/curl-request.adoc[]
The resulting response is similar to the following:
include::{snippets}/quartz/jobs/http-response.adoc[]
[[quartz-job-groups-response-structure]]
=== Response Structure
The response contains the registered job names for each group.
The following table describes the structure of the response:
[cols="3,1,3"]
include::{snippets}/quartz/jobs/response-fields.adoc[]
[[quartz-trigger-groups]]
== Retrieving Registered Trigger Names
To retrieve the list of registered trigger names, make a `GET` request to `/actuator/quartz/triggers`, as shown in the following curl-based example:
include::{snippets}/quartz/triggers/curl-request.adoc[]
The resulting response is similar to the following:
include::{snippets}/quartz/triggers/http-response.adoc[]
[[quartz-trigger-groups-response-structure]]
=== Response Structure
The response contains the registered trigger names for each group.
The following table describes the structure of the response:
[cols="3,1,3"]
include::{snippets}/quartz/triggers/response-fields.adoc[]
[[quartz-job-group]]
== Retrieving Overview of a Job Group
To retrieve an overview of the jobs in a particular group, make a `GET` request to `/actuator/quartz/jobs/\{groupName}`, as shown in the following curl-based example:
include::{snippets}/quartz/job-group/curl-request.adoc[]
The preceding example retrieves the summary for jobs in the `samples` group.
The resulting response is similar to the following:
include::{snippets}/quartz/job-group/http-response.adoc[]
[[quartz-job-group-response-structure]]
=== Response Structure
The response contains an overview of jobs in a particular group.
The following table describes the structure of the response:
[cols="3,1,3"]
include::{snippets}/quartz/job-group/response-fields.adoc[]
[[quartz-trigger-group]]
== Retrieving Overview of a Trigger Group
To retrieve an overview of the triggers in a particular group, make a `GET` request to `/actuator/quartz/triggers/\{groupName}`, as shown in the following curl-based example:
include::{snippets}/quartz/trigger-group/curl-request.adoc[]
The preceding example retrieves the summary for triggers in the `tests` group.
The resulting response is similar to the following:
include::{snippets}/quartz/trigger-group/http-response.adoc[]
[[quartz-trigger-group-response-structure]]
=== Response Structure
The response contains an overview of triggers in a particular group.
Trigger implementation specific details are available.
The following table describes the structure of the response:
[cols="3,1,3"]
include::{snippets}/quartz/trigger-group/response-fields.adoc[]
[[quartz-job]]
== Retrieving Details of a Job
To retrieve the details about a particular job, make a `GET` request to `/actuator/quartz/jobs/\{groupName}/\{jobName}`, as shown in the following curl-based example:
include::{snippets}/quartz/job-details/curl-request.adoc[]
The preceding example retrieves the details of the job identified by the `samples` group and `jobOne` name.
The resulting response is similar to the following:
include::{snippets}/quartz/job-details/http-response.adoc[]
If a key in the data map is identified as sensitive, its value is sanitized.
[[quartz-job-response-structure]]
=== Response Structure
The response contains the full details of a job including a summary of the triggers associated with it, if any.
The triggers are sorted by next fire time and priority.
The following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}/quartz/job-details/response-fields.adoc[]
[[quartz-trigger]]
== Retrieving Details of a Trigger
To retrieve the details about a particular trigger, make a `GET` request to `/actuator/quartz/triggers/\{groupName}/\{triggerName}`, as shown in the following curl-based example:
include::{snippets}/quartz/trigger-details-cron/curl-request.adoc[]
The preceding example retrieves the details of trigger identified by the `samples` group and `example` name.
The resulting response has a common structure and a specific additional object according to the trigger implementation.
There are five supported types:
* `cron` for `CronTrigger`
* `simple` for `SimpleTrigger`
* `dailyTimeInterval` for `DailyTimeIntervalTrigger`
* `calendarInterval` for `CalendarIntervalTrigger`
* `custom` for any other trigger implementations
[[quartz-trigger-cron]]
=== Cron Trigger Response Structure
A cron trigger defines the cron expression that is used to determine when it has to fire.
The resulting response for such a trigger implementation is similar to the following:
include::{snippets}/quartz/trigger-details-cron/http-response.adoc[]
The following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}/quartz/trigger-details-cron/response-fields.adoc[]
[[quartz-trigger-simple]]
=== Simple Trigger Response Structure
A simple trigger is used to fire a Job at a given moment in time, and optionally repeated at a specified interval.
The resulting response for such a trigger implementation is similar to the following:
include::{snippets}/quartz/trigger-details-simple/http-response.adoc[]
The following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}/quartz/trigger-details-simple/response-fields.adoc[]
[[quartz-trigger-daily-time-interval]]
=== Daily Time Interval Trigger Response Structure
A daily time interval trigger is used to fire a Job based upon daily repeating time intervals.
The resulting response for such a trigger implementation is similar to the following:
include::{snippets}/quartz/trigger-details-daily-time-interval/http-response.adoc[]
The following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}/quartz/trigger-details-daily-time-interval/response-fields.adoc[]
[[quartz-trigger-calendar-interval]]
=== Calendar Interval Trigger Response Structure
A daily time interval trigger is used to fire a Job based upon repeating calendar time intervals.
The resulting response for such a trigger implementation is similar to the following:
include::{snippets}/quartz/trigger-details-calendar-interval/http-response.adoc[]
The following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}/quartz/trigger-details-calendar-interval/response-fields.adoc[]
[[quartz-trigger-custom]]
=== Custom Trigger Response Structure
A custom trigger is any other implementation.
The resulting response for such a trigger implementation is similar to the following:
include::{snippets}/quartz/trigger-details-custom/http-response.adoc[]
The following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}/quartz/trigger-details-custom/response-fields.adoc[]

@ -67,6 +67,7 @@ include::endpoints/loggers.adoc[leveloffset=+1]
include::endpoints/mappings.adoc[leveloffset=+1] include::endpoints/mappings.adoc[leveloffset=+1]
include::endpoints/metrics.adoc[leveloffset=+1] include::endpoints/metrics.adoc[leveloffset=+1]
include::endpoints/prometheus.adoc[leveloffset=+1] include::endpoints/prometheus.adoc[leveloffset=+1]
include::endpoints/quartz.adoc[leveloffset=+1]
include::endpoints/scheduledtasks.adoc[leveloffset=+1] include::endpoints/scheduledtasks.adoc[leveloffset=+1]
include::endpoints/sessions.adoc[leveloffset=+1] include::endpoints/sessions.adoc[leveloffset=+1]
include::endpoints/shutdown.adoc[leveloffset=+1] include::endpoints/shutdown.adoc[leveloffset=+1]

@ -0,0 +1,60 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.quartz;
import org.quartz.Scheduler;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.quartz.QuartzEndpoint;
import org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link QuartzEndpoint}.
*
* @author Vedran Pavic
* @author Stephane Nicoll
* @since 2.5.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Scheduler.class)
@AutoConfigureAfter(QuartzAutoConfiguration.class)
@ConditionalOnAvailableEndpoint(endpoint = QuartzEndpoint.class)
public class QuartzEndpointAutoConfiguration {
@Bean
@ConditionalOnBean(Scheduler.class)
@ConditionalOnMissingBean
public QuartzEndpoint quartzEndpoint(Scheduler scheduler) {
return new QuartzEndpoint(scheduler);
}
@Bean
@ConditionalOnBean(QuartzEndpoint.class)
@ConditionalOnMissingBean
public QuartzEndpointWebExtension quartzEndpointWebExtension(QuartzEndpoint endpoint) {
return new QuartzEndpointWebExtension(endpoint);
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Auto-configuration for actuator Quartz Scheduler concerns.
*/
package org.springframework.boot.actuate.autoconfigure.quartz;

@ -80,6 +80,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat.TomcatMetricsA
org.springframework.boot.actuate.autoconfigure.mongo.MongoHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.mongo.MongoHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.mongo.MongoReactiveHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.mongo.MongoReactiveHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.quartz.QuartzEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.r2dbc.ConnectionFactoryHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.r2dbc.ConnectionFactoryHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.redis.RedisHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.redis.RedisHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.redis.RedisReactiveHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.redis.RedisReactiveHealthContributorAutoConfiguration,\

@ -0,0 +1,448 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.TimeZone;
import org.junit.jupiter.api.Test;
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.IntervalUnit;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobKey;
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.Trigger.TriggerState;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher;
import org.quartz.spi.OperableTrigger;
import org.springframework.boot.actuate.quartz.QuartzEndpoint;
import org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.restdocs.payload.FieldDescriptor;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.scheduling.quartz.DelegatingJob;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for generating documentation describing the {@link QuartzEndpoint}.
*
* @author Vedran Pavic
* @author Stephane Nicoll
*/
class QuartzEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
private static final TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris");
private static final JobDetail jobOne = JobBuilder.newJob(DelegatingJob.class).withIdentity("jobOne", "samples")
.withDescription("A sample job").usingJobData("user", "admin").usingJobData("password", "secret").build();
private static final JobDetail jobTwo = JobBuilder.newJob(Job.class).withIdentity("jobTwo", "samples").build();
private static final JobDetail jobThree = JobBuilder.newJob(Job.class).withIdentity("jobThree", "tests").build();
private static final CronTrigger triggerOne = TriggerBuilder.newTrigger().forJob(jobOne).withPriority(3)
.withDescription("3AM on weekdays").withIdentity("3am-weekdays", "samples")
.withSchedule(
CronScheduleBuilder.atHourAndMinuteOnGivenDaysOfWeek(3, 0, 1, 2, 3, 4, 5).inTimeZone(timeZone))
.build();
private static final SimpleTrigger triggerTwo = TriggerBuilder.newTrigger().forJob(jobOne).withPriority(7)
.withDescription("Once a day").withIdentity("every-day", "samples")
.withSchedule(SimpleScheduleBuilder.repeatHourlyForever(24)).build();
private static final CalendarIntervalTrigger triggerThree = TriggerBuilder.newTrigger().forJob(jobTwo)
.withDescription("Once a week").withIdentity("once-a-week", "samples")
.withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withIntervalInWeeks(1)
.inTimeZone(timeZone))
.build();
private static final DailyTimeIntervalTrigger triggerFour = TriggerBuilder.newTrigger().forJob(jobThree)
.withDescription("Every hour between 9AM and 6PM on Tuesday and Thursday")
.withIdentity("every-hour-tue-thu")
.withSchedule(DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()
.onDaysOfTheWeek(Calendar.TUESDAY, Calendar.THURSDAY)
.startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0))
.endingDailyAt(TimeOfDay.hourAndMinuteOfDay(18, 0)).withInterval(1, IntervalUnit.HOUR))
.build();
private static final List<FieldDescriptor> triggerSummary = Arrays.asList(previousFireTime(""), nextFireTime(""),
priority(""));
private static final List<FieldDescriptor> cronTriggerSummary = Arrays.asList(
fieldWithPath("expression").description("Cron expression to use."),
fieldWithPath("timeZone").type(JsonFieldType.STRING).optional()
.description("Time zone for which the expression will be resolved, if any."));
private static final List<FieldDescriptor> simpleTriggerSummary = Collections
.singletonList(fieldWithPath("interval").description("Interval, in milliseconds, between two executions."));
private static final List<FieldDescriptor> dailyTimeIntervalTriggerSummary = Arrays.asList(
fieldWithPath("interval").description(
"Interval, in milliseconds, added to the fire time in order to calculate the time of the next trigger repeat."),
fieldWithPath("daysOfWeek").type(JsonFieldType.ARRAY)
.description("An array of days of the week upon which to fire."),
fieldWithPath("startTimeOfDay").type(JsonFieldType.STRING)
.description("Time of day to start firing at the given interval, if any."),
fieldWithPath("endTimeOfDay").type(JsonFieldType.STRING)
.description("Time of day to complete firing at the given interval, if any."));
private static final List<FieldDescriptor> calendarIntervalTriggerSummary = Arrays.asList(
fieldWithPath("interval").description(
"Interval, in milliseconds, added to the fire time in order to calculate the time of the next trigger repeat."),
fieldWithPath("timeZone").type(JsonFieldType.STRING)
.description("Time zone within which time calculations will be performed, if any."));
private static final List<FieldDescriptor> customTriggerSummary = Collections.singletonList(
fieldWithPath("trigger").description("A toString representation of the custom trigger instance."));
private static final FieldDescriptor[] commonCronDetails = new FieldDescriptor[] {
fieldWithPath("group").description("Name of the group."),
fieldWithPath("name").description("Name of the trigger."),
fieldWithPath("description").description("Description of the trigger, if any."),
fieldWithPath("state")
.description("State of the trigger, can be NONE, NORMAL, PAUSED, COMPLETE, ERROR, or BLOCKED."),
fieldWithPath("type").description(
"Type of the trigger, determine the key of the object containing implementation-specific details."),
fieldWithPath("calendarName").description("Name of the Calendar associated with this Trigger, if any."),
startTime(""), endTime(""), previousFireTime(""), nextFireTime(""), priority(""),
fieldWithPath("finalFireTime").optional().type(JsonFieldType.STRING)
.description("Last time at which the Trigger will fire, if any."),
fieldWithPath("data").optional().type(JsonFieldType.OBJECT)
.description("Job data map keyed by name, if any.") };
@MockBean
private Scheduler scheduler;
@Test
void quartzReport() throws Exception {
mockJobs(jobOne, jobTwo, jobThree);
mockTriggers(triggerOne, triggerTwo, triggerThree, triggerFour);
this.mockMvc.perform(get("/actuator/quartz")).andExpect(status().isOk())
.andDo(document("quartz/report",
responseFields(fieldWithPath("jobs.groups").description("An array of job group names."),
fieldWithPath("triggers.groups").description("An array of trigger group names."))));
}
@Test
void quartzJobs() throws Exception {
mockJobs(jobOne, jobTwo, jobThree);
this.mockMvc.perform(get("/actuator/quartz/jobs")).andExpect(status().isOk()).andDo(
document("quartz/jobs", responseFields(fieldWithPath("groups").description("Job groups keyed by name."),
fieldWithPath("groups.*.jobs").description("An array of job names."))));
}
@Test
void quartzTriggers() throws Exception {
mockTriggers(triggerOne, triggerTwo, triggerThree, triggerFour);
this.mockMvc.perform(get("/actuator/quartz/triggers")).andExpect(status().isOk())
.andDo(document("quartz/triggers",
responseFields(fieldWithPath("groups").description("Trigger groups keyed by name."),
fieldWithPath("groups.*.paused").description("Whether this trigger group is paused."),
fieldWithPath("groups.*.triggers").description("An array of trigger names."))));
}
@Test
void quartzJobGroup() throws Exception {
mockJobs(jobOne, jobTwo, jobThree);
this.mockMvc.perform(get("/actuator/quartz/jobs/samples")).andExpect(status().isOk())
.andDo(document("quartz/job-group",
responseFields(fieldWithPath("group").description("Name of the group."),
fieldWithPath("jobs").description("Job details keyed by name."),
fieldWithPath("jobs.*.className")
.description("Fully qualified name of the job implementation."))));
}
@Test
void quartzTriggerGroup() throws Exception {
CronTrigger cron = triggerOne.getTriggerBuilder().startAt(fromUtc("2020-11-30T17:00:00Z"))
.endAt(fromUtc("2020-12-30T03:00:00Z")).withIdentity("3am-week", "tests").build();
setPreviousNextFireTime(cron, "2020-12-04T03:00:00Z", "2020-12-07T03:00:00Z");
SimpleTrigger simple = triggerTwo.getTriggerBuilder().withIdentity("every-day", "tests").build();
setPreviousNextFireTime(simple, null, "2020-12-04T12:00:00Z");
CalendarIntervalTrigger calendarInterval = triggerThree.getTriggerBuilder().withIdentity("once-a-week", "tests")
.startAt(fromUtc("2019-07-10T14:00:00Z")).endAt(fromUtc("2023-01-01T12:00:00Z")).build();
setPreviousNextFireTime(calendarInterval, "2020-12-02T14:00:00Z", "2020-12-08T14:00:00Z");
DailyTimeIntervalTrigger tueThuTrigger = triggerFour.getTriggerBuilder().withIdentity("tue-thu", "tests")
.build();
Trigger customTrigger = mock(Trigger.class);
given(customTrigger.getKey()).willReturn(TriggerKey.triggerKey("once-a-year-custom", "tests"));
given(customTrigger.toString()).willReturn("com.example.CustomTrigger@fdsfsd");
given(customTrigger.getPriority()).willReturn(10);
given(customTrigger.getPreviousFireTime()).willReturn(fromUtc("2020-07-14T16:00:00Z"));
given(customTrigger.getNextFireTime()).willReturn(fromUtc("2021-07-14T16:00:00Z"));
mockTriggers(cron, simple, calendarInterval, tueThuTrigger, customTrigger);
this.mockMvc.perform(get("/actuator/quartz/triggers/tests")).andExpect(status().isOk()).andDo(document(
"quartz/trigger-group",
responseFields(fieldWithPath("group").description("Name of the group."),
fieldWithPath("paused").description("Whether the group is paused."),
fieldWithPath("triggers.cron").description("Cron triggers keyed by name, if any."),
fieldWithPath("triggers.simple").description("Simple triggers keyed by name, if any."),
fieldWithPath("triggers.dailyTimeInterval")
.description("Daily time interval triggers keyed by name, if any."),
fieldWithPath("triggers.calendarInterval")
.description("Calendar interval triggers keyed by name, if any."),
fieldWithPath("triggers.custom").description("Any other triggers keyed by name, if any."))
.andWithPrefix("triggers.cron.*.", concat(triggerSummary, cronTriggerSummary))
.andWithPrefix("triggers.simple.*.", concat(triggerSummary, simpleTriggerSummary))
.andWithPrefix("triggers.dailyTimeInterval.*.",
concat(triggerSummary, dailyTimeIntervalTriggerSummary))
.andWithPrefix("triggers.calendarInterval.*.",
concat(triggerSummary, calendarIntervalTriggerSummary))
.andWithPrefix("triggers.custom.*.", concat(triggerSummary, customTriggerSummary))));
}
@Test
void quartzJob() throws Exception {
mockJobs(jobOne);
CronTrigger firstTrigger = triggerOne.getTriggerBuilder().build();
setPreviousNextFireTime(firstTrigger, null, "2020-12-07T03:00:00Z");
SimpleTrigger secondTrigger = triggerTwo.getTriggerBuilder().build();
setPreviousNextFireTime(secondTrigger, "2020-12-04T03:00:00Z", "2020-12-04T12:00:00Z");
mockTriggers(firstTrigger, secondTrigger);
given(this.scheduler.getTriggersOfJob(jobOne.getKey()))
.willAnswer((invocation) -> Arrays.asList(firstTrigger, secondTrigger));
this.mockMvc.perform(get("/actuator/quartz/jobs/samples/jobOne")).andExpect(status().isOk()).andDo(document(
"quartz/job-details",
responseFields(fieldWithPath("group").description("Name of the group."),
fieldWithPath("name").description("Name of the job."),
fieldWithPath("description").description("Description of the job, if any."),
fieldWithPath("className").description("Fully qualified name of the job implementation."),
fieldWithPath("durable")
.description("Whether the job should remain stored after it is orphaned."),
fieldWithPath("requestRecovery").description(
"Whether the job should be re-executed if a 'recovery' or 'fail-over' situation is encountered."),
fieldWithPath("data.*").description("Job data map as key/value pairs, if any."),
fieldWithPath("triggers").description("An array of triggers associated to the job, if any."),
fieldWithPath("triggers.[].group").description("Name of the the trigger group."),
fieldWithPath("triggers.[].name").description("Name of the the trigger."),
previousFireTime("triggers.[]."), nextFireTime("triggers.[]."), priority("triggers.[]."))));
}
@Test
void quartzTriggerCron() throws Exception {
setupTriggerDetails(triggerOne.getTriggerBuilder(), TriggerState.NORMAL);
this.mockMvc.perform(get("/actuator/quartz/triggers/samples/example")).andExpect(status().isOk())
.andDo(document("quartz/trigger-details-cron",
responseFields(commonCronDetails)
.and(fieldWithPath("cron").description("Cron trigger specific details."))
.andWithPrefix("cron.", cronTriggerSummary)));
}
@Test
void quartzTriggerSimple() throws Exception {
setupTriggerDetails(triggerTwo.getTriggerBuilder(), TriggerState.NORMAL);
this.mockMvc.perform(get("/actuator/quartz/triggers/samples/example")).andExpect(status().isOk())
.andDo(document("quartz/trigger-details-simple",
responseFields(commonCronDetails)
.and(fieldWithPath("simple").description("Simple trigger specific details."))
.andWithPrefix("simple.", simpleTriggerSummary)
.and(repeatCount("simple."), timesTriggered("simple."))));
}
@Test
void quartzTriggerCalendarInterval() throws Exception {
setupTriggerDetails(triggerThree.getTriggerBuilder(), TriggerState.NORMAL);
this.mockMvc.perform(get("/actuator/quartz/triggers/samples/example")).andExpect(status().isOk())
.andDo(document("quartz/trigger-details-calendar-interval", responseFields(commonCronDetails)
.and(fieldWithPath("calendarInterval")
.description("Calendar interval trigger specific details."))
.andWithPrefix("calendarInterval.", calendarIntervalTriggerSummary)
.and(timesTriggered("calendarInterval."),
fieldWithPath("calendarInterval.preserveHourOfDayAcrossDaylightSavings").description(
"Whether to fire the trigger at the same time of day, regardless of daylight "
+ "saving time transitions."),
fieldWithPath("calendarInterval.skipDayIfHourDoesNotExist").description(
"Whether to skip if the hour of the day does not exist on a given day."))));
}
@Test
void quartzTriggerDailyTimeInterval() throws Exception {
setupTriggerDetails(triggerFour.getTriggerBuilder(), TriggerState.PAUSED);
this.mockMvc.perform(get("/actuator/quartz/triggers/samples/example")).andExpect(status().isOk())
.andDo(document("quartz/trigger-details-daily-time-interval",
responseFields(commonCronDetails)
.and(fieldWithPath("dailyTimeInterval")
.description("Daily time interval trigger specific details."))
.andWithPrefix("dailyTimeInterval.", dailyTimeIntervalTriggerSummary)
.and(repeatCount("dailyTimeInterval."), timesTriggered("dailyTimeInterval."))));
}
@Test
void quartzTriggerCustom() throws Exception {
Trigger trigger = mock(Trigger.class);
given(trigger.getKey()).willReturn(TriggerKey.triggerKey("example", "samples"));
given(trigger.getDescription()).willReturn("Example trigger.");
given(trigger.toString()).willReturn("com.example.CustomTrigger@fdsfsd");
given(trigger.getPriority()).willReturn(10);
given(trigger.getStartTime()).willReturn(fromUtc("2020-11-30T17:00:00Z"));
given(trigger.getEndTime()).willReturn(fromUtc("2020-12-30T03:00:00Z"));
given(trigger.getCalendarName()).willReturn("bankHolidays");
given(trigger.getPreviousFireTime()).willReturn(fromUtc("2020-12-04T03:00:00Z"));
given(trigger.getNextFireTime()).willReturn(fromUtc("2020-12-07T03:00:00Z"));
given(this.scheduler.getTriggerState(trigger.getKey())).willReturn(TriggerState.NORMAL);
mockTriggers(trigger);
this.mockMvc.perform(get("/actuator/quartz/triggers/samples/example")).andExpect(status().isOk())
.andDo(document("quartz/trigger-details-custom",
responseFields(commonCronDetails)
.and(fieldWithPath("custom").description("Custom trigger specific details."))
.andWithPrefix("custom.", customTriggerSummary)));
}
private <T extends Trigger> T setupTriggerDetails(TriggerBuilder<T> builder, TriggerState state)
throws SchedulerException {
T trigger = builder.withIdentity("example", "samples").withDescription("Example trigger")
.startAt(fromUtc("2020-11-30T17:00:00Z")).modifiedByCalendar("bankHolidays")
.endAt(fromUtc("2020-12-30T03:00:00Z")).build();
setPreviousNextFireTime(trigger, "2020-12-04T03:00:00Z", "2020-12-07T03:00:00Z");
given(this.scheduler.getTriggerState(trigger.getKey())).willReturn(state);
mockTriggers(trigger);
return trigger;
}
private static FieldDescriptor startTime(String prefix) {
return fieldWithPath(prefix + "startTime").description("Time at which the Trigger should take effect, if any.");
}
private static FieldDescriptor endTime(String prefix) {
return fieldWithPath(prefix + "endTime").description(
"Time at which the Trigger should quit repeating, regardless of any remaining repeats, if any.");
}
private static FieldDescriptor previousFireTime(String prefix) {
return fieldWithPath(prefix + "previousFireTime").optional().type(JsonFieldType.STRING)
.description("Last time the trigger fired, if any.");
}
private static FieldDescriptor nextFireTime(String prefix) {
return fieldWithPath(prefix + "nextFireTime").optional().type(JsonFieldType.STRING)
.description("Next time at which the Trigger is scheduled to fire, if any.");
}
private static FieldDescriptor priority(String prefix) {
return fieldWithPath(prefix + "priority")
.description("Priority to use if two triggers have the same scheduled fire time.");
}
private static FieldDescriptor repeatCount(String prefix) {
return fieldWithPath(prefix + "repeatCount")
.description("Number of times the trigger should repeat, or -1 to repeat indefinitely.");
}
private static FieldDescriptor timesTriggered(String prefix) {
return fieldWithPath(prefix + "timesTriggered").description("Number of times the trigger has already fired.");
}
private static List<FieldDescriptor> concat(List<FieldDescriptor> initial, List<FieldDescriptor> additionalFields) {
List<FieldDescriptor> result = new ArrayList<>(initial);
result.addAll(additionalFields);
return result;
}
private void mockJobs(JobDetail... jobs) throws SchedulerException {
MultiValueMap<String, JobKey> jobKeys = new LinkedMultiValueMap<>();
for (JobDetail jobDetail : jobs) {
JobKey key = jobDetail.getKey();
given(this.scheduler.getJobDetail(key)).willReturn(jobDetail);
jobKeys.add(key.getGroup(), key);
}
given(this.scheduler.getJobGroupNames()).willReturn(new ArrayList<>(jobKeys.keySet()));
for (Entry<String, List<JobKey>> entry : jobKeys.entrySet()) {
given(this.scheduler.getJobKeys(GroupMatcher.jobGroupEquals(entry.getKey())))
.willReturn(new LinkedHashSet<>(entry.getValue()));
}
}
private void mockTriggers(Trigger... triggers) throws SchedulerException {
MultiValueMap<String, TriggerKey> triggerKeys = new LinkedMultiValueMap<>();
for (Trigger trigger : triggers) {
TriggerKey key = trigger.getKey();
given(this.scheduler.getTrigger(key)).willReturn(trigger);
triggerKeys.add(key.getGroup(), key);
}
given(this.scheduler.getTriggerGroupNames()).willReturn(new ArrayList<>(triggerKeys.keySet()));
for (Entry<String, List<TriggerKey>> entry : triggerKeys.entrySet()) {
given(this.scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals(entry.getKey())))
.willReturn(new LinkedHashSet<>(entry.getValue()));
}
}
private <T extends Trigger> T setPreviousNextFireTime(T trigger, String previousFireTime, String nextFireTime) {
OperableTrigger operableTrigger = (OperableTrigger) trigger;
if (previousFireTime != null) {
operableTrigger.setPreviousFireTime(fromUtc(previousFireTime));
}
if (nextFireTime != null) {
operableTrigger.setNextFireTime(fromUtc(nextFireTime));
}
return trigger;
}
private static Date fromUtc(String utcTime) {
return Date.from(Instant.parse(utcTime));
}
@Configuration(proxyBeanMethods = false)
@Import(BaseDocumentationConfiguration.class)
static class TestConfiguration {
@Bean
QuartzEndpoint endpoint(Scheduler scheduler) {
return new QuartzEndpoint(scheduler);
}
@Bean
QuartzEndpointWebExtension endpointWebExtension(QuartzEndpoint endpoint) {
return new QuartzEndpointWebExtension(endpoint);
}
}
}

@ -0,0 +1,92 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.quartz;
import org.junit.jupiter.api.Test;
import org.quartz.Scheduler;
import org.springframework.boot.actuate.quartz.QuartzEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link QuartzEndpointAutoConfiguration}.
*
* @author Vedran Pavic
* @author Stephane Nicoll
*/
class QuartzEndpointAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(QuartzEndpointAutoConfiguration.class));
@Test
void endpointIsAutoConfigured() {
this.contextRunner.withBean(Scheduler.class, () -> mock(Scheduler.class))
.withPropertyValues("management.endpoints.web.exposure.include=quartz")
.run((context) -> assertThat(context).hasSingleBean(QuartzEndpoint.class));
}
@Test
void endpointIsNotAutoConfiguredIfSchedulerIsNotAvailable() {
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=quartz")
.run((context) -> assertThat(context).doesNotHaveBean(QuartzEndpoint.class));
}
@Test
void endpointNotAutoConfiguredWhenNotExposed() {
this.contextRunner.withBean(Scheduler.class, () -> mock(Scheduler.class))
.run((context) -> assertThat(context).doesNotHaveBean(QuartzEndpoint.class));
}
@Test
void endpointCanBeDisabled() {
this.contextRunner.withBean(Scheduler.class, () -> mock(Scheduler.class))
.withPropertyValues("management.endpoint.quartz.enabled:false")
.run((context) -> assertThat(context).doesNotHaveBean(QuartzEndpoint.class));
}
@Test
void endpointBacksOffWhenUserProvidedEndpointIsPresent() {
this.contextRunner.withUserConfiguration(CustomEndpointConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(QuartzEndpoint.class).hasBean("customEndpoint"));
}
@Configuration(proxyBeanMethods = false)
static class CustomEndpointConfiguration {
@Bean
CustomEndpoint customEndpoint() {
return new CustomEndpoint();
}
}
private static final class CustomEndpoint extends QuartzEndpoint {
private CustomEndpoint() {
super(mock(Scheduler.class));
}
}
}

@ -48,6 +48,7 @@ dependencies {
optional("org.mongodb:mongodb-driver-reactivestreams") optional("org.mongodb:mongodb-driver-reactivestreams")
optional("org.mongodb:mongodb-driver-sync") optional("org.mongodb:mongodb-driver-sync")
optional("org.neo4j.driver:neo4j-java-driver") optional("org.neo4j.driver:neo4j-java-driver")
optional("org.quartz-scheduler:quartz")
optional("org.springframework:spring-jdbc") optional("org.springframework:spring-jdbc")
optional("org.springframework:spring-messaging") optional("org.springframework:spring-messaging")
optional("org.springframework:spring-webflux") optional("org.springframework:spring-webflux")

@ -0,0 +1,802 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.quartz;
import java.time.Duration;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.quartz.CalendarIntervalTrigger;
import org.quartz.CronTrigger;
import org.quartz.DailyTimeIntervalTrigger;
import org.quartz.DateBuilder.IntervalUnit;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;
import org.quartz.TimeOfDay;
import org.quartz.Trigger;
import org.quartz.Trigger.TriggerState;
import org.quartz.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.util.Assert;
/**
* {@link Endpoint} to expose Quartz Scheduler jobs and triggers.
*
* @author Vedran Pavic
* @author Stephane Nicoll
* @since 2.5.0
*/
@Endpoint(id = "quartz")
public class QuartzEndpoint {
private static final Comparator<Trigger> TRIGGER_COMPARATOR = Comparator
.comparing(Trigger::getNextFireTime, Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(Comparator.comparingInt(Trigger::getPriority).reversed());
private final Scheduler scheduler;
private final Sanitizer sanitizer;
/**
* Create an instance for the specified {@link Scheduler} and {@link Sanitizer}.
* @param scheduler the scheduler to use to retrieve jobs and triggers details
* @param sanitizer the sanitizer to use to sanitize data maps
*/
public QuartzEndpoint(Scheduler scheduler, Sanitizer sanitizer) {
Assert.notNull(scheduler, "Scheduler must not be null");
Assert.notNull(sanitizer, "Sanitizer must not be null");
this.scheduler = scheduler;
this.sanitizer = sanitizer;
}
/**
* Create an instance for the specified {@link Scheduler} using a default
* {@link Sanitizer}.
* @param scheduler the scheduler to use to retrieve jobs and triggers details
*/
public QuartzEndpoint(Scheduler scheduler) {
this(scheduler, new Sanitizer());
}
/**
* Return the available job and trigger group names.
* @return a report of the available group names
* @throws SchedulerException if retrieving the information from the scheduler failed
*/
@ReadOperation
public QuartzReport quartzReport() throws SchedulerException {
return new QuartzReport(new GroupNames(this.scheduler.getJobGroupNames()),
new GroupNames(this.scheduler.getTriggerGroupNames()));
}
/**
* Return the available job names, identified by group name.
* @return the available job names
* @throws SchedulerException if retrieving the information from the scheduler failed
*/
public QuartzGroups quartzJobGroups() throws SchedulerException {
Map<String, Object> result = new LinkedHashMap<>();
for (String groupName : this.scheduler.getJobGroupNames()) {
List<String> jobs = this.scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName)).stream()
.map((key) -> key.getName()).collect(Collectors.toList());
result.put(groupName, Collections.singletonMap("jobs", jobs));
}
return new QuartzGroups(result);
}
/**
* Return the available trigger names, identified by group name.
* @return the available trigger names
* @throws SchedulerException if retrieving the information from the scheduler failed
*/
public QuartzGroups quartzTriggerGroups() throws SchedulerException {
Map<String, Object> result = new LinkedHashMap<>();
Set<String> pausedTriggerGroups = this.scheduler.getPausedTriggerGroups();
for (String groupName : this.scheduler.getTriggerGroupNames()) {
Map<String, Object> groupDetails = new LinkedHashMap<>();
groupDetails.put("paused", pausedTriggerGroups.contains(groupName));
groupDetails.put("triggers", this.scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals(groupName))
.stream().map((key) -> key.getName()).collect(Collectors.toList()));
result.put(groupName, groupDetails);
}
return new QuartzGroups(result);
}
/**
* Return a summary of the jobs group with the specified name or {@code null} if no
* such group exists.
* @param group the name of a jobs group
* @return a summary of the jobs in the given {@code group}
* @throws SchedulerException if retrieving the information from the scheduler failed
*/
public QuartzJobGroupSummary quartzJobGroupSummary(String group) throws SchedulerException {
List<JobDetail> jobs = findJobsByGroup(group);
if (jobs.isEmpty() && !this.scheduler.getJobGroupNames().contains(group)) {
return null;
}
Map<String, QuartzJobSummary> result = new LinkedHashMap<>();
for (JobDetail job : jobs) {
result.put(job.getKey().getName(), QuartzJobSummary.of(job));
}
return new QuartzJobGroupSummary(group, result);
}
private List<JobDetail> findJobsByGroup(String group) throws SchedulerException {
List<JobDetail> jobs = new ArrayList<>();
Set<JobKey> jobKeys = this.scheduler.getJobKeys(GroupMatcher.jobGroupEquals(group));
for (JobKey jobKey : jobKeys) {
jobs.add(this.scheduler.getJobDetail(jobKey));
}
return jobs;
}
/**
* Return a summary of the triggers group with the specified name or {@code null} if
* no such group exists.
* @param group the name of a triggers group
* @return a summary of the triggers in the given {@code group}
* @throws SchedulerException if retrieving the information from the scheduler failed
*/
public QuartzTriggerGroupSummary quartzTriggerGroupSummary(String group) throws SchedulerException {
List<Trigger> triggers = findTriggersByGroup(group);
if (triggers.isEmpty() && !this.scheduler.getTriggerGroupNames().contains(group)) {
return null;
}
Map<TriggerType, Map<String, Object>> result = new LinkedHashMap<>();
triggers.forEach((trigger) -> {
TriggerDescription triggerDescription = TriggerDescription.of(trigger);
Map<String, Object> triggerTypes = result.computeIfAbsent(triggerDescription.getType(),
(key) -> new LinkedHashMap<>());
triggerTypes.put(trigger.getKey().getName(), triggerDescription.buildSummary(true));
});
boolean paused = this.scheduler.getPausedTriggerGroups().contains(group);
return new QuartzTriggerGroupSummary(group, paused, result);
}
private List<Trigger> findTriggersByGroup(String group) throws SchedulerException {
List<Trigger> triggers = new ArrayList<>();
Set<TriggerKey> triggerKeys = this.scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals(group));
for (TriggerKey triggerKey : triggerKeys) {
triggers.add(this.scheduler.getTrigger(triggerKey));
}
return triggers;
}
/**
* Return the {@link QuartzJobDetails details of the job} identified with the given
* group name and job name.
* @param groupName the name of the group
* @param jobName the name of the job
* @return the details of the job or {@code null} if such job does not exist
* @throws SchedulerException if retrieving the information from the scheduler failed
*/
public QuartzJobDetails quartzJob(String groupName, String jobName) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(jobName, groupName);
JobDetail jobDetail = this.scheduler.getJobDetail(jobKey);
if (jobDetail != null) {
List<? extends Trigger> triggers = this.scheduler.getTriggersOfJob(jobKey);
return new QuartzJobDetails(jobDetail.getKey().getGroup(), jobDetail.getKey().getName(),
jobDetail.getDescription(), jobDetail.getJobClass().getName(), jobDetail.isDurable(),
jobDetail.requestsRecovery(), sanitizeJobDataMap(jobDetail.getJobDataMap()),
extractTriggersSummary(triggers));
}
return null;
}
private static List<Map<String, Object>> extractTriggersSummary(List<? extends Trigger> triggers) {
List<Trigger> triggersToSort = new ArrayList<>(triggers);
triggersToSort.sort(TRIGGER_COMPARATOR);
List<Map<String, Object>> result = new ArrayList<>();
triggersToSort.forEach((trigger) -> {
Map<String, Object> triggerSummary = new LinkedHashMap<>();
triggerSummary.put("group", trigger.getKey().getGroup());
triggerSummary.put("name", trigger.getKey().getName());
triggerSummary.putAll(TriggerDescription.of(trigger).buildSummary(false));
result.add(triggerSummary);
});
return result;
}
/**
* Return the details of the trigger identified by the given group name and trigger
* name.
* @param groupName the name of the group
* @param triggerName the name of the trigger
* @return the details of the trigger or {@code null} if such trigger does not exist
* @throws SchedulerException if retrieving the information from the scheduler failed
*/
public Map<String, Object> quartzTrigger(String groupName, String triggerName) throws SchedulerException {
TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, groupName);
Trigger trigger = this.scheduler.getTrigger(triggerKey);
return (trigger != null) ? TriggerDescription.of(trigger).buildDetails(
this.scheduler.getTriggerState(triggerKey), sanitizeJobDataMap(trigger.getJobDataMap())) : null;
}
private static Duration getIntervalDuration(long amount, IntervalUnit unit) {
return temporalUnit(unit).getDuration().multipliedBy(amount);
}
private static LocalTime getLocalTime(TimeOfDay timeOfDay) {
return (timeOfDay != null) ? LocalTime.of(timeOfDay.getHour(), timeOfDay.getMinute(), timeOfDay.getSecond())
: null;
}
private Map<String, Object> sanitizeJobDataMap(JobDataMap dataMap) {
if (dataMap != null) {
Map<String, Object> map = new LinkedHashMap<>(dataMap.getWrappedMap());
map.replaceAll(this.sanitizer::sanitize);
return map;
}
return null;
}
private static TemporalUnit temporalUnit(IntervalUnit unit) {
switch (unit) {
case DAY:
return ChronoUnit.DAYS;
case HOUR:
return ChronoUnit.HOURS;
case MINUTE:
return ChronoUnit.MINUTES;
case MONTH:
return ChronoUnit.MONTHS;
case SECOND:
return ChronoUnit.SECONDS;
case MILLISECOND:
return ChronoUnit.MILLIS;
case WEEK:
return ChronoUnit.WEEKS;
case YEAR:
return ChronoUnit.YEARS;
default:
throw new IllegalArgumentException("Unknown IntervalUnit");
}
}
/**
* A report of available job and trigger group names, primarily intended for
* serialization to JSON.
*/
public static final class QuartzReport {
private final GroupNames jobs;
private final GroupNames triggers;
QuartzReport(GroupNames jobs, GroupNames triggers) {
this.jobs = jobs;
this.triggers = triggers;
}
public GroupNames getJobs() {
return this.jobs;
}
public GroupNames getTriggers() {
return this.triggers;
}
}
/**
* A set of group names, primarily intended for serialization to JSON.
*/
public static class GroupNames {
private final Set<String> groups;
public GroupNames(List<String> groups) {
this.groups = new LinkedHashSet<>(groups);
}
public Set<String> getGroups() {
return this.groups;
}
}
/**
* A summary for each group identified by name, primarily intended for serialization
* to JSON.
*/
public static class QuartzGroups {
private final Map<String, Object> groups;
public QuartzGroups(Map<String, Object> groups) {
this.groups = groups;
}
public Map<String, Object> getGroups() {
return this.groups;
}
}
/**
* A summary report of the {@link JobDetail jobs} in a given group.
*/
public static final class QuartzJobGroupSummary {
private final String group;
private final Map<String, QuartzJobSummary> jobs;
private QuartzJobGroupSummary(String group, Map<String, QuartzJobSummary> jobs) {
this.group = group;
this.jobs = jobs;
}
public String getGroup() {
return this.group;
}
public Map<String, QuartzJobSummary> getJobs() {
return this.jobs;
}
}
/**
* Details of a {@link Job Quartz Job}, primarily intended for serialization to JSON.
*/
public static final class QuartzJobSummary {
private final String className;
private QuartzJobSummary(JobDetail job) {
this.className = job.getJobClass().getName();
}
private static QuartzJobSummary of(JobDetail job) {
return new QuartzJobSummary(job);
}
public String getClassName() {
return this.className;
}
}
/**
* Details of a {@link Job Quartz Job}, primarily intended for serialization to JSON.
*/
public static final class QuartzJobDetails {
private final String group;
private final String name;
private final String description;
private final String className;
private final boolean durable;
private final boolean requestRecovery;
private final Map<String, Object> data;
private final List<Map<String, Object>> triggers;
QuartzJobDetails(String group, String name, String description, String className, boolean durable,
boolean requestRecovery, Map<String, Object> data, List<Map<String, Object>> triggers) {
this.group = group;
this.name = name;
this.description = description;
this.className = className;
this.durable = durable;
this.requestRecovery = requestRecovery;
this.data = data;
this.triggers = triggers;
}
public String getGroup() {
return this.group;
}
public String getName() {
return this.name;
}
public String getDescription() {
return this.description;
}
public String getClassName() {
return this.className;
}
public boolean isDurable() {
return this.durable;
}
public boolean isRequestRecovery() {
return this.requestRecovery;
}
public Map<String, Object> getData() {
return this.data;
}
public List<Map<String, Object>> getTriggers() {
return this.triggers;
}
}
/**
* A summary report of the {@link Trigger triggers} in a given group.
*/
public static final class QuartzTriggerGroupSummary {
private final String group;
private final boolean paused;
private final Triggers triggers;
private QuartzTriggerGroupSummary(String group, boolean paused,
Map<TriggerType, Map<String, Object>> descriptionsByType) {
this.group = group;
this.paused = paused;
this.triggers = new Triggers(descriptionsByType);
}
public String getGroup() {
return this.group;
}
public boolean isPaused() {
return this.paused;
}
public Triggers getTriggers() {
return this.triggers;
}
public static final class Triggers {
private final Map<String, Object> cron;
private final Map<String, Object> simple;
private final Map<String, Object> dailyTimeInterval;
private final Map<String, Object> calendarInterval;
private final Map<String, Object> custom;
private Triggers(Map<TriggerType, Map<String, Object>> descriptionsByType) {
this.cron = descriptionsByType.getOrDefault(TriggerType.CRON, Collections.emptyMap());
this.dailyTimeInterval = descriptionsByType.getOrDefault(TriggerType.DAILY_INTERVAL,
Collections.emptyMap());
this.calendarInterval = descriptionsByType.getOrDefault(TriggerType.CALENDAR_INTERVAL,
Collections.emptyMap());
this.simple = descriptionsByType.getOrDefault(TriggerType.SIMPLE, Collections.emptyMap());
this.custom = descriptionsByType.getOrDefault(TriggerType.CUSTOM_TRIGGER, Collections.emptyMap());
}
public Map<String, Object> getCron() {
return this.cron;
}
public Map<String, Object> getSimple() {
return this.simple;
}
public Map<String, Object> getDailyTimeInterval() {
return this.dailyTimeInterval;
}
public Map<String, Object> getCalendarInterval() {
return this.calendarInterval;
}
public Map<String, Object> getCustom() {
return this.custom;
}
}
}
private enum TriggerType {
CRON("cron"),
CUSTOM_TRIGGER("custom"),
CALENDAR_INTERVAL("calendarInterval"),
DAILY_INTERVAL("dailyTimeInterval"),
SIMPLE("simple");
private final String id;
TriggerType(String id) {
this.id = id;
}
public String getId() {
return this.id;
}
}
/**
* Base class for descriptions of a {@link Trigger}.
*/
public abstract static class TriggerDescription {
private static final Map<Class<? extends Trigger>, Function<Trigger, TriggerDescription>> DESCRIBERS = new LinkedHashMap<>();
static {
DESCRIBERS.put(CronTrigger.class, (trigger) -> new CronTriggerDescription((CronTrigger) trigger));
DESCRIBERS.put(SimpleTrigger.class, (trigger) -> new SimpleTriggerDescription((SimpleTrigger) trigger));
DESCRIBERS.put(DailyTimeIntervalTrigger.class,
(trigger) -> new DailyTimeIntervalTriggerDescription((DailyTimeIntervalTrigger) trigger));
DESCRIBERS.put(CalendarIntervalTrigger.class,
(trigger) -> new CalendarIntervalTriggerDescription((CalendarIntervalTrigger) trigger));
}
private final Trigger trigger;
private final TriggerType type;
private static TriggerDescription of(Trigger trigger) {
return DESCRIBERS.entrySet().stream().filter((entry) -> entry.getKey().isInstance(trigger))
.map((entry) -> entry.getValue().apply(trigger)).findFirst()
.orElse(new CustomTriggerDescription(trigger));
}
protected TriggerDescription(Trigger trigger, TriggerType type) {
this.trigger = trigger;
this.type = type;
}
/**
* Build the summary of the trigger.
* @param addTriggerSpecificSummary whether to add trigger-implementation specific
* summary.
* @return basic properties of the trigger
*/
public Map<String, Object> buildSummary(boolean addTriggerSpecificSummary) {
Map<String, Object> summary = new LinkedHashMap<>();
putIfNoNull(summary, "previousFireTime", this.trigger.getPreviousFireTime());
putIfNoNull(summary, "nextFireTime", this.trigger.getNextFireTime());
summary.put("priority", this.trigger.getPriority());
if (addTriggerSpecificSummary) {
appendSummary(summary);
}
return summary;
}
/**
* Append trigger-implementation specific summary items to the specified
* {@code content}.
* @param content the summary of the trigger
*/
protected abstract void appendSummary(Map<String, Object> content);
/**
* Build the full details of the trigger.
* @param triggerState the current state of the trigger
* @param sanitizedDataMap a sanitized data map or {@code null}
* @return all properties of the trigger
*/
public Map<String, Object> buildDetails(TriggerState triggerState, Map<String, Object> sanitizedDataMap) {
Map<String, Object> details = new LinkedHashMap<>();
details.put("group", this.trigger.getKey().getGroup());
details.put("name", this.trigger.getKey().getName());
putIfNoNull(details, "description", this.trigger.getDescription());
details.put("state", triggerState);
details.put("type", getType().getId());
putIfNoNull(details, "calendarName", this.trigger.getCalendarName());
putIfNoNull(details, "startTime", this.trigger.getStartTime());
putIfNoNull(details, "endTime", this.trigger.getEndTime());
putIfNoNull(details, "previousFireTime", this.trigger.getPreviousFireTime());
putIfNoNull(details, "nextFireTime", this.trigger.getNextFireTime());
putIfNoNull(details, "priority", this.trigger.getPriority());
putIfNoNull(details, "finalFireTime", this.trigger.getFinalFireTime());
putIfNoNull(details, "data", sanitizedDataMap);
Map<String, Object> typeDetails = new LinkedHashMap<>();
appendDetails(typeDetails);
details.put(getType().getId(), typeDetails);
return details;
}
/**
* Append trigger-implementation specific details to the specified
* {@code content}.
* @param content the details of the trigger
*/
protected abstract void appendDetails(Map<String, Object> content);
protected void putIfNoNull(Map<String, Object> content, String key, Object value) {
if (value != null) {
content.put(key, value);
}
}
protected Trigger getTrigger() {
return this.trigger;
}
protected TriggerType getType() {
return this.type;
}
}
/**
* A description of a {@link CronTrigger}.
*/
public static final class CronTriggerDescription extends TriggerDescription {
private final CronTrigger trigger;
public CronTriggerDescription(CronTrigger trigger) {
super(trigger, TriggerType.CRON);
this.trigger = trigger;
}
@Override
protected void appendSummary(Map<String, Object> content) {
content.put("expression", this.trigger.getCronExpression());
putIfNoNull(content, "timeZone", this.trigger.getTimeZone());
}
@Override
protected void appendDetails(Map<String, Object> content) {
appendSummary(content);
}
}
/**
* A description of a {@link SimpleTrigger}.
*/
public static final class SimpleTriggerDescription extends TriggerDescription {
private final SimpleTrigger trigger;
public SimpleTriggerDescription(SimpleTrigger trigger) {
super(trigger, TriggerType.SIMPLE);
this.trigger = trigger;
}
@Override
protected void appendSummary(Map<String, Object> content) {
content.put("interval", this.trigger.getRepeatInterval());
}
@Override
protected void appendDetails(Map<String, Object> content) {
appendSummary(content);
content.put("repeatCount", this.trigger.getRepeatCount());
content.put("timesTriggered", this.trigger.getTimesTriggered());
}
}
/**
* A description of a {@link DailyTimeIntervalTrigger}.
*/
public static final class DailyTimeIntervalTriggerDescription extends TriggerDescription {
private final DailyTimeIntervalTrigger trigger;
public DailyTimeIntervalTriggerDescription(DailyTimeIntervalTrigger trigger) {
super(trigger, TriggerType.DAILY_INTERVAL);
this.trigger = trigger;
}
@Override
protected void appendSummary(Map<String, Object> content) {
content.put("interval",
getIntervalDuration(this.trigger.getRepeatInterval(), this.trigger.getRepeatIntervalUnit())
.toMillis());
putIfNoNull(content, "daysOfWeek", this.trigger.getDaysOfWeek());
putIfNoNull(content, "startTimeOfDay", getLocalTime(this.trigger.getStartTimeOfDay()));
putIfNoNull(content, "endTimeOfDay", getLocalTime(this.trigger.getEndTimeOfDay()));
}
@Override
protected void appendDetails(Map<String, Object> content) {
appendSummary(content);
content.put("repeatCount", this.trigger.getRepeatCount());
content.put("timesTriggered", this.trigger.getTimesTriggered());
}
}
/**
* A description of a {@link CalendarIntervalTrigger}.
*/
public static final class CalendarIntervalTriggerDescription extends TriggerDescription {
private final CalendarIntervalTrigger trigger;
public CalendarIntervalTriggerDescription(CalendarIntervalTrigger trigger) {
super(trigger, TriggerType.CALENDAR_INTERVAL);
this.trigger = trigger;
}
@Override
protected void appendSummary(Map<String, Object> content) {
content.put("interval",
getIntervalDuration(this.trigger.getRepeatInterval(), this.trigger.getRepeatIntervalUnit())
.toMillis());
putIfNoNull(content, "timeZone", this.trigger.getTimeZone());
}
@Override
protected void appendDetails(Map<String, Object> content) {
appendSummary(content);
content.put("timesTriggered", this.trigger.getTimesTriggered());
content.put("preserveHourOfDayAcrossDaylightSavings",
this.trigger.isPreserveHourOfDayAcrossDaylightSavings());
content.put("skipDayIfHourDoesNotExist", this.trigger.isSkipDayIfHourDoesNotExist());
}
}
/**
* A description of a custom {@link Trigger}.
*/
public static final class CustomTriggerDescription extends TriggerDescription {
public CustomTriggerDescription(Trigger trigger) {
super(trigger, TriggerType.CUSTOM_TRIGGER);
}
@Override
protected void appendSummary(Map<String, Object> content) {
content.put("trigger", getTrigger().toString());
}
@Override
protected void appendDetails(Map<String, Object> content) {
appendSummary(content);
}
}
}

@ -0,0 +1,87 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.quartz;
import org.quartz.SchedulerException;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzGroups;
/**
* {@link EndpointWebExtension @EndpointWebExtension} for the {@link QuartzEndpoint}.
*
* @author Stephane Nicoll
* @since 2.5.0
*/
@EndpointWebExtension(endpoint = QuartzEndpoint.class)
public class QuartzEndpointWebExtension {
private final QuartzEndpoint delegate;
public QuartzEndpointWebExtension(QuartzEndpoint delegate) {
this.delegate = delegate;
}
@ReadOperation
public WebEndpointResponse<QuartzGroups> quartzJobOrTriggerGroups(@Selector String jobsOrTriggers)
throws SchedulerException {
return handle(jobsOrTriggers, this.delegate::quartzJobGroups, this.delegate::quartzTriggerGroups);
}
@ReadOperation
public WebEndpointResponse<Object> quartzJobOrTriggerGroup(@Selector String jobsOrTriggers, @Selector String group)
throws SchedulerException {
return handle(jobsOrTriggers, () -> this.delegate.quartzJobGroupSummary(group),
() -> this.delegate.quartzTriggerGroupSummary(group));
}
@ReadOperation
public WebEndpointResponse<Object> quartzJobOrTrigger(@Selector String jobsOrTriggers, @Selector String group,
@Selector String name) throws SchedulerException {
return handle(jobsOrTriggers, () -> this.delegate.quartzJob(group, name),
() -> this.delegate.quartzTrigger(group, name));
}
private <T> WebEndpointResponse<T> handle(String jobsOrTriggers, ResponseSupplier<T> jobAction,
ResponseSupplier<T> triggerAction) throws SchedulerException {
if ("jobs".equals(jobsOrTriggers)) {
return handleNull(jobAction.get());
}
if ("triggers".equals(jobsOrTriggers)) {
return handleNull(triggerAction.get());
}
return new WebEndpointResponse<>(WebEndpointResponse.STATUS_BAD_REQUEST);
}
private <T> WebEndpointResponse<T> handleNull(T value) {
if (value != null) {
return new WebEndpointResponse<>(value);
}
return new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND);
}
@FunctionalInterface
private interface ResponseSupplier<T> {
T get() throws SchedulerException;
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Actuator support for Quartz Scheduler.
*/
package org.springframework.boot.actuate.quartz;

@ -0,0 +1,710 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.quartz;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TimeZone;
import java.util.stream.Stream;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.assertj.core.api.InstanceOfAssertFactory;
import org.assertj.core.api.MapAssert;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
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.IntervalUnit;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobKey;
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.Trigger.TriggerState;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher;
import org.quartz.spi.OperableTrigger;
import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobDetails;
import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobGroupSummary;
import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobSummary;
import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzReport;
import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzTriggerGroupSummary;
import org.springframework.scheduling.quartz.DelegatingJob;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link QuartzEndpoint}.
*
* @author Vedran Pavic
* @author Stephane Nicoll
*/
class QuartzEndpointTests {
private static final JobDetail jobOne = JobBuilder.newJob(Job.class).withIdentity("jobOne").build();
private static final JobDetail jobTwo = JobBuilder.newJob(DelegatingJob.class).withIdentity("jobTwo").build();
private static final JobDetail jobThree = JobBuilder.newJob(Job.class).withIdentity("jobThree", "samples").build();
private static final Trigger triggerOne = TriggerBuilder.newTrigger().forJob(jobOne).withIdentity("triggerOne")
.build();
private static final Trigger triggerTwo = TriggerBuilder.newTrigger().forJob(jobOne).withIdentity("triggerTwo")
.build();
private static final Trigger triggerThree = TriggerBuilder.newTrigger().forJob(jobThree)
.withIdentity("triggerThree", "samples").build();
private final Scheduler scheduler;
private final QuartzEndpoint endpoint;
QuartzEndpointTests() {
this.scheduler = mock(Scheduler.class);
this.endpoint = new QuartzEndpoint(this.scheduler);
}
@Test
void quartzReport() throws SchedulerException {
given(this.scheduler.getJobGroupNames()).willReturn(Arrays.asList("jobSamples", "DEFAULT"));
given(this.scheduler.getTriggerGroupNames()).willReturn(Collections.singletonList("triggerSamples"));
QuartzReport quartzReport = this.endpoint.quartzReport();
assertThat(quartzReport.getJobs().getGroups()).containsOnly("jobSamples", "DEFAULT");
assertThat(quartzReport.getTriggers().getGroups()).containsOnly("triggerSamples");
verify(this.scheduler).getJobGroupNames();
verify(this.scheduler).getTriggerGroupNames();
verifyNoMoreInteractions(this.scheduler);
}
@Test
void quartzReportWithNoJob() throws SchedulerException {
given(this.scheduler.getJobGroupNames()).willReturn(Collections.emptyList());
given(this.scheduler.getTriggerGroupNames()).willReturn(Arrays.asList("triggerSamples", "DEFAULT"));
QuartzReport quartzReport = this.endpoint.quartzReport();
assertThat(quartzReport.getJobs().getGroups()).isEmpty();
assertThat(quartzReport.getTriggers().getGroups()).containsOnly("triggerSamples", "DEFAULT");
}
@Test
void quartzReportWithNoTrigger() throws SchedulerException {
given(this.scheduler.getJobGroupNames()).willReturn(Collections.singletonList("jobSamples"));
given(this.scheduler.getTriggerGroupNames()).willReturn(Collections.emptyList());
QuartzReport quartzReport = this.endpoint.quartzReport();
assertThat(quartzReport.getJobs().getGroups()).containsOnly("jobSamples");
assertThat(quartzReport.getTriggers().getGroups()).isEmpty();
}
@Test
void quartzJobGroupsWithExistingGroups() throws SchedulerException {
mockJobs(jobOne, jobTwo, jobThree);
Map<String, Object> jobGroups = this.endpoint.quartzJobGroups().getGroups();
assertThat(jobGroups).containsOnlyKeys("DEFAULT", "samples");
assertThat(jobGroups).extractingByKey("DEFAULT", nestedMap())
.containsOnly(entry("jobs", Arrays.asList("jobOne", "jobTwo")));
assertThat(jobGroups).extractingByKey("samples", nestedMap())
.containsOnly(entry("jobs", Collections.singletonList("jobThree")));
}
@Test
void quartzJobGroupsWithNoGroup() throws SchedulerException {
given(this.scheduler.getJobGroupNames()).willReturn(Collections.emptyList());
Map<String, Object> jobGroups = this.endpoint.quartzJobGroups().getGroups();
assertThat(jobGroups).isEmpty();
}
@Test
void quartzTriggerGroupsWithExistingGroups() throws SchedulerException {
mockTriggers(triggerOne, triggerTwo, triggerThree);
given(this.scheduler.getPausedTriggerGroups()).willReturn(Collections.singleton("samples"));
Map<String, Object> triggerGroups = this.endpoint.quartzTriggerGroups().getGroups();
assertThat(triggerGroups).containsOnlyKeys("DEFAULT", "samples");
assertThat(triggerGroups).extractingByKey("DEFAULT", nestedMap()).containsOnly(entry("paused", false),
entry("triggers", Arrays.asList("triggerOne", "triggerTwo")));
assertThat(triggerGroups).extractingByKey("samples", nestedMap()).containsOnly(entry("paused", true),
entry("triggers", Collections.singletonList("triggerThree")));
}
@Test
void quartzTriggerGroupsWithNoGroup() throws SchedulerException {
given(this.scheduler.getTriggerGroupNames()).willReturn(Collections.emptyList());
Map<String, Object> triggerGroups = this.endpoint.quartzTriggerGroups().getGroups();
assertThat(triggerGroups).isEmpty();
}
@Test
void quartzJobGroupSummaryWithInvalidGroup() throws SchedulerException {
given(this.scheduler.getJobGroupNames()).willReturn(Collections.singletonList("DEFAULT"));
QuartzJobGroupSummary summary = this.endpoint.quartzJobGroupSummary("unknown");
assertThat(summary).isNull();
}
@Test
void quartzJobGroupSummaryWithEmptyGroup() throws SchedulerException {
given(this.scheduler.getJobGroupNames()).willReturn(Collections.singletonList("samples"));
given(this.scheduler.getJobKeys(GroupMatcher.jobGroupEquals("samples"))).willReturn(Collections.emptySet());
QuartzJobGroupSummary summary = this.endpoint.quartzJobGroupSummary("samples");
assertThat(summary).isNotNull();
assertThat(summary.getGroup()).isEqualTo("samples");
assertThat(summary.getJobs()).isEmpty();
}
@Test
void quartzJobGroupSummaryWithJobs() throws SchedulerException {
mockJobs(jobOne, jobTwo);
QuartzJobGroupSummary summary = this.endpoint.quartzJobGroupSummary("DEFAULT");
assertThat(summary).isNotNull();
assertThat(summary.getGroup()).isEqualTo("DEFAULT");
Map<String, QuartzJobSummary> jobSummaries = summary.getJobs();
assertThat(jobSummaries).containsOnlyKeys("jobOne", "jobTwo");
assertThat(jobSummaries.get("jobOne").getClassName()).isEqualTo(Job.class.getName());
assertThat(jobSummaries.get("jobTwo").getClassName()).isEqualTo(DelegatingJob.class.getName());
}
@Test
void quartzTriggerGroupSummaryWithInvalidGroup() throws SchedulerException {
given(this.scheduler.getTriggerGroupNames()).willReturn(Collections.singletonList("DEFAULT"));
QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("unknown");
assertThat(summary).isNull();
}
@Test
void quartzTriggerGroupSummaryWithEmptyGroup() throws SchedulerException {
given(this.scheduler.getTriggerGroupNames()).willReturn(Collections.singletonList("samples"));
given(this.scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals("samples")))
.willReturn(Collections.emptySet());
QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples");
assertThat(summary).isNotNull();
assertThat(summary.getGroup()).isEqualTo("samples");
assertThat(summary.isPaused()).isFalse();
assertThat(summary.getTriggers().getCron()).isEmpty();
assertThat(summary.getTriggers().getSimple()).isEmpty();
assertThat(summary.getTriggers().getDailyTimeInterval()).isEmpty();
assertThat(summary.getTriggers().getCalendarInterval()).isEmpty();
assertThat(summary.getTriggers().getCustom()).isEmpty();
}
@Test
void quartzTriggerGroupSummaryWithCronTrigger() throws SchedulerException {
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("3am-every-day", "samples")
.withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0)).build();
mockTriggers(cronTrigger);
QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples");
assertThat(summary.getGroup()).isEqualTo("samples");
assertThat(summary.isPaused()).isFalse();
assertThat(summary.getTriggers().getCron()).containsOnlyKeys("3am-every-day");
assertThat(summary.getTriggers().getSimple()).isEmpty();
assertThat(summary.getTriggers().getDailyTimeInterval()).isEmpty();
assertThat(summary.getTriggers().getCalendarInterval()).isEmpty();
assertThat(summary.getTriggers().getCustom()).isEmpty();
}
@Test
void quartzTriggerGroupSummaryWithCronTriggerDetails() throws SchedulerException {
Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z"));
Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z"));
TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris");
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("3am-every-day", "samples").withPriority(3)
.withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0).inTimeZone(timeZone)).build();
((OperableTrigger) cronTrigger).setPreviousFireTime(previousFireTime);
((OperableTrigger) cronTrigger).setNextFireTime(nextFireTime);
mockTriggers(cronTrigger);
QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples");
Map<String, Object> triggers = summary.getTriggers().getCron();
assertThat(triggers).containsOnlyKeys("3am-every-day");
assertThat(triggers).extractingByKey("3am-every-day", nestedMap()).containsOnly(
entry("previousFireTime", previousFireTime), entry("nextFireTime", nextFireTime), entry("priority", 3),
entry("expression", "0 0 3 ? * *"), entry("timeZone", timeZone));
}
@Test
void quartzTriggerGroupSummaryWithSimpleTrigger() throws SchedulerException {
SimpleTrigger simpleTrigger = TriggerBuilder.newTrigger().withIdentity("every-hour", "samples")
.withSchedule(SimpleScheduleBuilder.repeatHourlyForever(1)).build();
mockTriggers(simpleTrigger);
QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples");
assertThat(summary.getGroup()).isEqualTo("samples");
assertThat(summary.isPaused()).isFalse();
assertThat(summary.getTriggers().getCron()).isEmpty();
assertThat(summary.getTriggers().getSimple()).containsOnlyKeys("every-hour");
assertThat(summary.getTriggers().getDailyTimeInterval()).isEmpty();
assertThat(summary.getTriggers().getCalendarInterval()).isEmpty();
assertThat(summary.getTriggers().getCustom()).isEmpty();
}
@Test
void quartzTriggerGroupSummaryWithSimpleTriggerDetails() throws SchedulerException {
Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z"));
Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z"));
SimpleTrigger simpleTrigger = TriggerBuilder.newTrigger().withIdentity("every-hour", "samples").withPriority(7)
.withSchedule(SimpleScheduleBuilder.repeatHourlyForever(1)).build();
((OperableTrigger) simpleTrigger).setPreviousFireTime(previousFireTime);
((OperableTrigger) simpleTrigger).setNextFireTime(nextFireTime);
mockTriggers(simpleTrigger);
QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples");
Map<String, Object> triggers = summary.getTriggers().getSimple();
assertThat(triggers).containsOnlyKeys("every-hour");
assertThat(triggers).extractingByKey("every-hour", nestedMap()).containsOnly(
entry("previousFireTime", previousFireTime), entry("nextFireTime", nextFireTime), entry("priority", 7),
entry("interval", 3600000L));
}
@Test
void quartzTriggerGroupSummaryWithDailyIntervalTrigger() throws SchedulerException {
DailyTimeIntervalTrigger trigger = TriggerBuilder.newTrigger().withIdentity("every-hour-9am", "samples")
.withSchedule(DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()
.startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0)).withInterval(1, IntervalUnit.HOUR))
.build();
mockTriggers(trigger);
QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples");
assertThat(summary.getGroup()).isEqualTo("samples");
assertThat(summary.isPaused()).isFalse();
assertThat(summary.getTriggers().getCron()).isEmpty();
assertThat(summary.getTriggers().getSimple()).isEmpty();
assertThat(summary.getTriggers().getDailyTimeInterval()).containsOnlyKeys("every-hour-9am");
assertThat(summary.getTriggers().getCalendarInterval()).isEmpty();
assertThat(summary.getTriggers().getCustom()).isEmpty();
}
@Test
void quartzTriggerGroupSummaryWithDailyIntervalTriggerDetails() throws SchedulerException {
Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z"));
Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z"));
DailyTimeIntervalTrigger trigger = TriggerBuilder.newTrigger().withIdentity("every-hour-tue-thu", "samples")
.withPriority(4)
.withSchedule(DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()
.onDaysOfTheWeek(Calendar.TUESDAY, Calendar.THURSDAY)
.startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0))
.endingDailyAt(TimeOfDay.hourAndMinuteOfDay(18, 0)).withInterval(1, IntervalUnit.HOUR))
.build();
((OperableTrigger) trigger).setPreviousFireTime(previousFireTime);
((OperableTrigger) trigger).setNextFireTime(nextFireTime);
mockTriggers(trigger);
QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples");
Map<String, Object> triggers = summary.getTriggers().getDailyTimeInterval();
assertThat(triggers).containsOnlyKeys("every-hour-tue-thu");
assertThat(triggers).extractingByKey("every-hour-tue-thu", nestedMap()).containsOnly(
entry("previousFireTime", previousFireTime), entry("nextFireTime", nextFireTime), entry("priority", 4),
entry("interval", 3600000L), entry("startTimeOfDay", LocalTime.of(9, 0)),
entry("endTimeOfDay", LocalTime.of(18, 0)),
entry("daysOfWeek", new LinkedHashSet<>(Arrays.asList(3, 5))));
}
@Test
void quartzTriggerGroupSummaryWithCalendarIntervalTrigger() throws SchedulerException {
CalendarIntervalTrigger trigger = TriggerBuilder.newTrigger().withIdentity("once-a-week", "samples")
.withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withIntervalInWeeks(1))
.build();
mockTriggers(trigger);
QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples");
assertThat(summary.getGroup()).isEqualTo("samples");
assertThat(summary.isPaused()).isFalse();
assertThat(summary.getTriggers().getCron()).isEmpty();
assertThat(summary.getTriggers().getSimple()).isEmpty();
assertThat(summary.getTriggers().getDailyTimeInterval()).isEmpty();
assertThat(summary.getTriggers().getCalendarInterval()).containsOnlyKeys("once-a-week");
assertThat(summary.getTriggers().getCustom()).isEmpty();
}
@Test
void quartzTriggerGroupSummaryWithCalendarIntervalTriggerDetails() throws SchedulerException {
TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris");
Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z"));
Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z"));
CalendarIntervalTrigger trigger = TriggerBuilder.newTrigger().withIdentity("once-a-week", "samples")
.withPriority(8).withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule()
.withIntervalInWeeks(1).inTimeZone(timeZone))
.build();
((OperableTrigger) trigger).setPreviousFireTime(previousFireTime);
((OperableTrigger) trigger).setNextFireTime(nextFireTime);
mockTriggers(trigger);
QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples");
Map<String, Object> triggers = summary.getTriggers().getCalendarInterval();
assertThat(triggers).containsOnlyKeys("once-a-week");
assertThat(triggers).extractingByKey("once-a-week", nestedMap()).containsOnly(
entry("previousFireTime", previousFireTime), entry("nextFireTime", nextFireTime), entry("priority", 8),
entry("interval", 604800000L), entry("timeZone", timeZone));
}
@Test
void quartzTriggerGroupSummaryWithCustomTrigger() throws SchedulerException {
Trigger trigger = mock(Trigger.class);
given(trigger.getKey()).willReturn(TriggerKey.triggerKey("custom", "samples"));
mockTriggers(trigger);
QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples");
assertThat(summary.getGroup()).isEqualTo("samples");
assertThat(summary.isPaused()).isFalse();
assertThat(summary.getTriggers().getCron()).isEmpty();
assertThat(summary.getTriggers().getSimple()).isEmpty();
assertThat(summary.getTriggers().getDailyTimeInterval()).isEmpty();
assertThat(summary.getTriggers().getCalendarInterval()).isEmpty();
assertThat(summary.getTriggers().getCustom()).containsOnlyKeys("custom");
}
@Test
void quartzTriggerGroupSummaryWithCustomTriggerDetails() throws SchedulerException {
Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z"));
Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z"));
Trigger trigger = mock(Trigger.class);
given(trigger.getKey()).willReturn(TriggerKey.triggerKey("custom", "samples"));
given(trigger.getPreviousFireTime()).willReturn(previousFireTime);
given(trigger.getNextFireTime()).willReturn(nextFireTime);
given(trigger.getPriority()).willReturn(9);
mockTriggers(trigger);
QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples");
Map<String, Object> triggers = summary.getTriggers().getCustom();
assertThat(triggers).containsOnlyKeys("custom");
assertThat(triggers).extractingByKey("custom", nestedMap()).containsOnly(
entry("previousFireTime", previousFireTime), entry("nextFireTime", nextFireTime), entry("priority", 9),
entry("trigger", trigger.toString()));
}
@Test
void quartzTriggerWithCronTrigger() throws SchedulerException {
Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z"));
Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z"));
TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris");
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity("3am-every-day", "samples").withPriority(3)
.withDescription("Sample description")
.withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0).inTimeZone(timeZone)).build();
((OperableTrigger) trigger).setPreviousFireTime(previousFireTime);
((OperableTrigger) trigger).setNextFireTime(nextFireTime);
mockTriggers(trigger);
given(this.scheduler.getTriggerState(TriggerKey.triggerKey("3am-every-day", "samples")))
.willReturn(TriggerState.NORMAL);
Map<String, Object> triggerDetails = this.endpoint.quartzTrigger("samples", "3am-every-day");
assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "3am-every-day"),
entry("description", "Sample description"), entry("type", "cron"), entry("state", TriggerState.NORMAL),
entry("priority", 3));
assertThat(triggerDetails).contains(entry("previousFireTime", previousFireTime),
entry("nextFireTime", nextFireTime));
assertThat(triggerDetails).doesNotContainKeys("simple", "dailyTimeInterval", "calendarInterval", "custom");
assertThat(triggerDetails).extractingByKey("cron", nestedMap()).containsOnly(entry("expression", "0 0 3 ? * *"),
entry("timeZone", timeZone));
}
@Test
void quartzTriggerWithSimpleTrigger() throws SchedulerException {
Date startTime = Date.from(Instant.parse("2020-01-01T09:00:00Z"));
Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z"));
Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z"));
Date endTime = Date.from(Instant.parse("2020-01-31T09:00:00Z"));
SimpleTrigger trigger = TriggerBuilder.newTrigger().withIdentity("every-hour", "samples").withPriority(20)
.withDescription("Every hour").startAt(startTime).endAt(endTime)
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInHours(1).withRepeatCount(2000))
.build();
((OperableTrigger) trigger).setPreviousFireTime(previousFireTime);
((OperableTrigger) trigger).setNextFireTime(nextFireTime);
mockTriggers(trigger);
given(this.scheduler.getTriggerState(TriggerKey.triggerKey("every-hour", "samples")))
.willReturn(TriggerState.COMPLETE);
Map<String, Object> triggerDetails = this.endpoint.quartzTrigger("samples", "every-hour");
assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "every-hour"),
entry("description", "Every hour"), entry("type", "simple"), entry("state", TriggerState.COMPLETE),
entry("priority", 20));
assertThat(triggerDetails).contains(entry("startTime", startTime), entry("previousFireTime", previousFireTime),
entry("nextFireTime", nextFireTime), entry("endTime", endTime));
assertThat(triggerDetails).doesNotContainKeys("cron", "dailyTimeInterval", "calendarInterval", "custom");
assertThat(triggerDetails).extractingByKey("simple", nestedMap()).containsOnly(entry("interval", 3600000L),
entry("repeatCount", 2000), entry("timesTriggered", 0));
}
@Test
void quartzTriggerWithDailyTimeIntervalTrigger() throws SchedulerException {
Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z"));
Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z"));
DailyTimeIntervalTrigger trigger = TriggerBuilder.newTrigger().withIdentity("every-hour-mon-wed", "samples")
.withDescription("Every working hour Mon Wed").withPriority(4)
.withSchedule(DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()
.onDaysOfTheWeek(Calendar.MONDAY, Calendar.WEDNESDAY)
.startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0))
.endingDailyAt(TimeOfDay.hourAndMinuteOfDay(18, 0)).withInterval(1, IntervalUnit.HOUR))
.build();
((OperableTrigger) trigger).setPreviousFireTime(previousFireTime);
((OperableTrigger) trigger).setNextFireTime(nextFireTime);
mockTriggers(trigger);
given(this.scheduler.getTriggerState(TriggerKey.triggerKey("every-hour-mon-wed", "samples")))
.willReturn(TriggerState.NORMAL);
Map<String, Object> triggerDetails = this.endpoint.quartzTrigger("samples", "every-hour-mon-wed");
assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "every-hour-mon-wed"),
entry("description", "Every working hour Mon Wed"), entry("type", "dailyTimeInterval"),
entry("state", TriggerState.NORMAL), entry("priority", 4));
assertThat(triggerDetails).contains(entry("previousFireTime", previousFireTime),
entry("nextFireTime", nextFireTime));
assertThat(triggerDetails).doesNotContainKeys("cron", "simple", "calendarInterval", "custom");
assertThat(triggerDetails).extractingByKey("dailyTimeInterval", nestedMap()).containsOnly(
entry("interval", 3600000L), entry("startTimeOfDay", LocalTime.of(9, 0)),
entry("endTimeOfDay", LocalTime.of(18, 0)),
entry("daysOfWeek", new LinkedHashSet<>(Arrays.asList(2, 4))), entry("repeatCount", -1),
entry("timesTriggered", 0));
}
@Test
void quartzTriggerWithCalendarTimeIntervalTrigger() throws SchedulerException {
TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris");
Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z"));
Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z"));
CalendarIntervalTrigger trigger = TriggerBuilder.newTrigger().withIdentity("once-a-week", "samples")
.withDescription("Once a week").withPriority(8)
.withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withIntervalInWeeks(1)
.inTimeZone(timeZone).preserveHourOfDayAcrossDaylightSavings(true))
.build();
((OperableTrigger) trigger).setPreviousFireTime(previousFireTime);
((OperableTrigger) trigger).setNextFireTime(nextFireTime);
mockTriggers(trigger);
given(this.scheduler.getTriggerState(TriggerKey.triggerKey("once-a-week", "samples")))
.willReturn(TriggerState.BLOCKED);
Map<String, Object> triggerDetails = this.endpoint.quartzTrigger("samples", "once-a-week");
assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "once-a-week"),
entry("description", "Once a week"), entry("type", "calendarInterval"),
entry("state", TriggerState.BLOCKED), entry("priority", 8));
assertThat(triggerDetails).contains(entry("previousFireTime", previousFireTime),
entry("nextFireTime", nextFireTime));
assertThat(triggerDetails).doesNotContainKeys("cron", "simple", "dailyTimeInterval", "custom");
assertThat(triggerDetails).extractingByKey("calendarInterval", nestedMap()).containsOnly(
entry("interval", 604800000L), entry("timeZone", timeZone),
entry("preserveHourOfDayAcrossDaylightSavings", true), entry("skipDayIfHourDoesNotExist", false),
entry("timesTriggered", 0));
}
@Test
void quartzTriggerWithCustomTrigger() throws SchedulerException {
Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z"));
Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z"));
Trigger trigger = mock(Trigger.class);
given(trigger.getKey()).willReturn(TriggerKey.triggerKey("custom", "samples"));
given(trigger.getPreviousFireTime()).willReturn(previousFireTime);
given(trigger.getNextFireTime()).willReturn(nextFireTime);
given(trigger.getPriority()).willReturn(9);
mockTriggers(trigger);
given(this.scheduler.getTriggerState(TriggerKey.triggerKey("custom", "samples")))
.willReturn(TriggerState.ERROR);
Map<String, Object> triggerDetails = this.endpoint.quartzTrigger("samples", "custom");
assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "custom"), entry("type", "custom"),
entry("state", TriggerState.ERROR), entry("priority", 9));
assertThat(triggerDetails).contains(entry("previousFireTime", previousFireTime),
entry("nextFireTime", nextFireTime));
assertThat(triggerDetails).doesNotContainKeys("cron", "simple", "calendarInterval", "dailyTimeInterval");
assertThat(triggerDetails).extractingByKey("custom", nestedMap())
.containsOnly(entry("trigger", trigger.toString()));
}
@Test
void quartzTriggerWithDataMap() throws SchedulerException {
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity("3am-every-day", "samples")
.withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0)).usingJobData("user", "user")
.usingJobData("password", "secret").usingJobData("url", "https://user:secret@example.com").build();
mockTriggers(trigger);
given(this.scheduler.getTriggerState(TriggerKey.triggerKey("3am-every-day", "samples")))
.willReturn(TriggerState.NORMAL);
Map<String, Object> triggerDetails = this.endpoint.quartzTrigger("samples", "3am-every-day");
assertThat(triggerDetails).extractingByKey("data", nestedMap()).containsOnly(entry("user", "user"),
entry("password", "******"), entry("url", "https://user:******@example.com"));
}
@ParameterizedTest(name = "unit {1}")
@MethodSource("intervalUnitParameters")
void canConvertIntervalUnit(int amount, IntervalUnit unit, Duration expectedDuration) throws SchedulerException {
CalendarIntervalTrigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger", "samples")
.withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withInterval(amount, unit))
.build();
mockTriggers(trigger);
Map<String, Object> triggerDetails = this.endpoint.quartzTrigger("samples", "trigger");
assertThat(triggerDetails).extractingByKey("calendarInterval", nestedMap())
.contains(entry("interval", expectedDuration.toMillis()));
}
static Stream<Arguments> intervalUnitParameters() {
return Stream.of(Arguments.of(3, IntervalUnit.DAY, Duration.ofDays(3)),
Arguments.of(2, IntervalUnit.HOUR, Duration.ofHours(2)),
Arguments.of(5, IntervalUnit.MINUTE, Duration.ofMinutes(5)),
Arguments.of(1, IntervalUnit.MONTH, ChronoUnit.MONTHS.getDuration()),
Arguments.of(30, IntervalUnit.SECOND, Duration.ofSeconds(30)),
Arguments.of(100, IntervalUnit.MILLISECOND, Duration.ofMillis(100)),
Arguments.of(1, IntervalUnit.WEEK, ChronoUnit.WEEKS.getDuration()),
Arguments.of(1, IntervalUnit.YEAR, ChronoUnit.YEARS.getDuration()));
}
@Test
void quartzJobWithoutTrigger() throws SchedulerException {
JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").withDescription("A sample job")
.storeDurably().requestRecovery(false).build();
mockJobs(job);
QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello");
assertThat(jobDetails.getGroup()).isEqualTo("samples");
assertThat(jobDetails.getName()).isEqualTo("hello");
assertThat(jobDetails.getDescription()).isEqualTo("A sample job");
assertThat(jobDetails.getClassName()).isEqualTo(Job.class.getName());
assertThat(jobDetails.isDurable()).isTrue();
assertThat(jobDetails.isRequestRecovery()).isFalse();
assertThat(jobDetails.getData()).isEmpty();
assertThat(jobDetails.getTriggers()).isEmpty();
}
@Test
void quartzJobWithTrigger() throws SchedulerException {
Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z"));
Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z"));
JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").build();
TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris");
Trigger trigger = TriggerBuilder.newTrigger().withIdentity("3am-every-day", "samples").withPriority(4)
.withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0).inTimeZone(timeZone)).build();
((OperableTrigger) trigger).setPreviousFireTime(previousFireTime);
((OperableTrigger) trigger).setNextFireTime(nextFireTime);
mockJobs(job);
mockTriggers(trigger);
given(this.scheduler.getTriggersOfJob(JobKey.jobKey("hello", "samples")))
.willAnswer((invocation) -> Collections.singletonList(trigger));
QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello");
assertThat(jobDetails.getTriggers()).hasSize(1);
Map<String, Object> triggerDetails = jobDetails.getTriggers().get(0);
assertThat(triggerDetails).containsOnly(entry("group", "samples"), entry("name", "3am-every-day"),
entry("previousFireTime", previousFireTime), entry("nextFireTime", nextFireTime), entry("priority", 4));
}
@Test
void quartzJobOrdersTriggersAccordingToNextFireTime() throws SchedulerException {
JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").build();
mockJobs(job);
Date triggerOneNextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z"));
CronTrigger triggerOne = TriggerBuilder.newTrigger().withIdentity("one", "samples").withPriority(5)
.withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0)).build();
((OperableTrigger) triggerOne).setNextFireTime(triggerOneNextFireTime);
Date triggerTwoNextFireTime = Date.from(Instant.parse("2020-12-01T02:00:00Z"));
CronTrigger triggerTwo = TriggerBuilder.newTrigger().withIdentity("two", "samples").withPriority(10)
.withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(2, 0)).build();
((OperableTrigger) triggerTwo).setNextFireTime(triggerTwoNextFireTime);
mockTriggers(triggerOne, triggerTwo);
given(this.scheduler.getTriggersOfJob(JobKey.jobKey("hello", "samples")))
.willAnswer((invocation) -> Arrays.asList(triggerOne, triggerTwo));
QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello");
assertThat(jobDetails.getTriggers()).hasSize(2);
assertThat(jobDetails.getTriggers().get(0)).containsEntry("name", "two");
assertThat(jobDetails.getTriggers().get(1)).containsEntry("name", "one");
}
@Test
void quartzJobOrdersTriggersAccordingNextFireTimeAndPriority() throws SchedulerException {
JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").build();
mockJobs(job);
Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z"));
CronTrigger triggerOne = TriggerBuilder.newTrigger().withIdentity("one", "samples").withPriority(3)
.withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0)).build();
((OperableTrigger) triggerOne).setNextFireTime(nextFireTime);
CronTrigger triggerTwo = TriggerBuilder.newTrigger().withIdentity("two", "samples").withPriority(7)
.withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0)).build();
((OperableTrigger) triggerTwo).setNextFireTime(nextFireTime);
mockTriggers(triggerOne, triggerTwo);
given(this.scheduler.getTriggersOfJob(JobKey.jobKey("hello", "samples")))
.willAnswer((invocation) -> Arrays.asList(triggerOne, triggerTwo));
QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello");
assertThat(jobDetails.getTriggers()).hasSize(2);
assertThat(jobDetails.getTriggers().get(0)).containsEntry("name", "two");
assertThat(jobDetails.getTriggers().get(1)).containsEntry("name", "one");
}
@Test
void quartzJobWithSensitiveDataMap() throws SchedulerException {
JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").usingJobData("user", "user")
.usingJobData("password", "secret").usingJobData("url", "https://user:secret@example.com").build();
mockJobs(job);
QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello");
assertThat(jobDetails.getData()).containsOnly(entry("user", "user"), entry("password", "******"),
entry("url", "https://user:******@example.com"));
}
@Test
void quartzJobWithSensitiveDataMapAndCustomSanitizier() throws SchedulerException {
JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").usingJobData("test", "value")
.usingJobData("secret", "value").build();
mockJobs(job);
Sanitizer sanitizer = mock(Sanitizer.class);
given(sanitizer.sanitize("test", "value")).willReturn("value");
given(sanitizer.sanitize("secret", "value")).willReturn("----");
QuartzJobDetails jobDetails = new QuartzEndpoint(this.scheduler, sanitizer).quartzJob("samples", "hello");
assertThat(jobDetails.getData()).containsOnly(entry("test", "value"), entry("secret", "----"));
verify(sanitizer).sanitize("test", "value");
verify(sanitizer).sanitize("secret", "value");
verifyNoMoreInteractions(sanitizer);
}
private void mockJobs(JobDetail... jobs) throws SchedulerException {
MultiValueMap<String, JobKey> jobKeys = new LinkedMultiValueMap<>();
for (JobDetail jobDetail : jobs) {
JobKey key = jobDetail.getKey();
given(this.scheduler.getJobDetail(key)).willReturn(jobDetail);
jobKeys.add(key.getGroup(), key);
}
given(this.scheduler.getJobGroupNames()).willReturn(new ArrayList<>(jobKeys.keySet()));
for (Entry<String, List<JobKey>> entry : jobKeys.entrySet()) {
given(this.scheduler.getJobKeys(GroupMatcher.jobGroupEquals(entry.getKey())))
.willReturn(new LinkedHashSet<>(entry.getValue()));
}
}
private void mockTriggers(Trigger... triggers) throws SchedulerException {
MultiValueMap<String, TriggerKey> triggerKeys = new LinkedMultiValueMap<>();
for (Trigger trigger : triggers) {
TriggerKey key = trigger.getKey();
given(this.scheduler.getTrigger(key)).willReturn(trigger);
triggerKeys.add(key.getGroup(), key);
}
given(this.scheduler.getTriggerGroupNames()).willReturn(new ArrayList<>(triggerKeys.keySet()));
for (Entry<String, List<TriggerKey>> entry : triggerKeys.entrySet()) {
given(this.scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals(entry.getKey())))
.willReturn(new LinkedHashSet<>(entry.getValue()));
}
}
@SuppressWarnings("rawtypes")
private static InstanceOfAssertFactory<Map, MapAssert<String, Object>> nestedMap() {
return InstanceOfAssertFactories.map(String.class, Object.class);
}
}

@ -0,0 +1,217 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.quartz;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map.Entry;
import net.minidev.json.JSONArray;
import org.quartz.CalendarIntervalScheduleBuilder;
import org.quartz.CalendarIntervalTrigger;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.Trigger.TriggerState;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.DelegatingJob;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Integration tests for {@link QuartzEndpoint} exposed by Jersey, Spring MVC, and
* WebFlux.
*
* @author Stephane Nicoll
*/
class QuartzEndpointWebIntegrationTests {
private static final JobDetail jobOne = JobBuilder.newJob(Job.class).withIdentity("jobOne", "samples")
.usingJobData(new JobDataMap(Collections.singletonMap("name", "test"))).withDescription("A sample job")
.build();
private static final JobDetail jobTwo = JobBuilder.newJob(DelegatingJob.class).withIdentity("jobTwo", "samples")
.build();
private static final JobDetail jobThree = JobBuilder.newJob(Job.class).withIdentity("jobThree").build();
private static final CronTrigger triggerOne = TriggerBuilder.newTrigger().withDescription("Once a day 3AM")
.withIdentity("triggerOne").withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0)).build();
private static final SimpleTrigger triggerTwo = TriggerBuilder.newTrigger().withDescription("Once a day")
.withIdentity("triggerTwo", "tests").withSchedule(SimpleScheduleBuilder.repeatHourlyForever(24)).build();
private static final CalendarIntervalTrigger triggerThree = TriggerBuilder.newTrigger()
.withDescription("Once a week").withIdentity("triggerThree", "tests")
.withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withIntervalInWeeks(1)).build();
@WebEndpointTest
void quartzReport(WebTestClient client) {
client.get().uri("/actuator/quartz").exchange().expectStatus().isOk().expectBody().jsonPath("jobs.groups")
.isEqualTo(new JSONArray().appendElement("samples").appendElement("DEFAULT"))
.jsonPath("triggers.groups").isEqualTo(new JSONArray().appendElement("DEFAULT").appendElement("tests"));
}
@WebEndpointTest
void quartzJobNames(WebTestClient client) {
client.get().uri("/actuator/quartz/jobs").exchange().expectStatus().isOk().expectBody()
.jsonPath("groups.samples.jobs")
.isEqualTo(new JSONArray().appendElement("jobOne").appendElement("jobTwo"))
.jsonPath("groups.DEFAULT.jobs").isEqualTo(new JSONArray().appendElement("jobThree"));
}
@WebEndpointTest
void quartzTriggerNames(WebTestClient client) {
client.get().uri("/actuator/quartz/triggers").exchange().expectStatus().isOk().expectBody()
.jsonPath("groups.DEFAULT.paused").isEqualTo(false).jsonPath("groups.DEFAULT.triggers")
.isEqualTo(new JSONArray().appendElement("triggerOne")).jsonPath("groups.tests.paused").isEqualTo(false)
.jsonPath("groups.tests.triggers")
.isEqualTo(new JSONArray().appendElement("triggerTwo").appendElement("triggerThree"));
}
@WebEndpointTest
void quartzTriggersOrJobsAreAllowed(WebTestClient client) {
client.get().uri("/actuator/quartz/something-elese").exchange().expectStatus().isBadRequest();
}
@WebEndpointTest
void quartzJobGroupSummary(WebTestClient client) {
client.get().uri("/actuator/quartz/jobs/samples").exchange().expectStatus().isOk().expectBody()
.jsonPath("group").isEqualTo("samples").jsonPath("jobs.jobOne.className").isEqualTo(Job.class.getName())
.jsonPath("jobs.jobTwo.className").isEqualTo(DelegatingJob.class.getName());
}
@WebEndpointTest
void quartzJobGroupSummaryWithUnknownGroup(WebTestClient client) {
client.get().uri("/actuator/quartz/jobs/does-not-exist").exchange().expectStatus().isNotFound();
}
@WebEndpointTest
void quartzTriggerGroupSummary(WebTestClient client) {
client.get().uri("/actuator/quartz/triggers/tests").exchange().expectStatus().isOk().expectBody()
.jsonPath("group").isEqualTo("tests").jsonPath("paused").isEqualTo("false").jsonPath("triggers.cron")
.isEmpty().jsonPath("triggers.simple.triggerTwo.interval").isEqualTo(86400000)
.jsonPath("triggers.dailyTimeInterval").isEmpty()
.jsonPath("triggers.calendarInterval.triggerThree.interval").isEqualTo(604800000)
.jsonPath("triggers.custom").isEmpty();
}
@WebEndpointTest
void quartzTriggerGroupSummaryWithUnknownGroup(WebTestClient client) {
client.get().uri("/actuator/quartz/triggers/does-not-exist").exchange().expectStatus().isNotFound();
}
@WebEndpointTest
void quartzJobDetail(WebTestClient client) {
client.get().uri("/actuator/quartz/jobs/samples/jobOne").exchange().expectStatus().isOk().expectBody()
.jsonPath("group").isEqualTo("samples").jsonPath("name").isEqualTo("jobOne").jsonPath("data.name")
.isEqualTo("test");
}
@WebEndpointTest
void quartzJobDetailWithUnknownKey(WebTestClient client) {
client.get().uri("/actuator/quartz/jobs/samples/does-not-exist").exchange().expectStatus().isNotFound();
}
@WebEndpointTest
void quartzTriggerDetail(WebTestClient client) {
client.get().uri("/actuator/quartz/triggers/DEFAULT/triggerOne").exchange().expectStatus().isOk().expectBody()
.jsonPath("group").isEqualTo("DEFAULT").jsonPath("name").isEqualTo("triggerOne").jsonPath("description")
.isEqualTo("Once a day 3AM").jsonPath("state").isEqualTo("NORMAL").jsonPath("type").isEqualTo("cron")
.jsonPath("simple").doesNotExist().jsonPath("calendarInterval").doesNotExist().jsonPath("dailyInterval")
.doesNotExist().jsonPath("custom").doesNotExist().jsonPath("cron.expression").isEqualTo("0 0 3 ? * *");
}
@WebEndpointTest
void quartzTriggerDetailWithUnknownKey(WebTestClient client) {
client.get().uri("/actuator/quartz/triggers/tests/does-not-exist").exchange().expectStatus().isNotFound();
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration {
@Bean
Scheduler scheduler() throws SchedulerException {
Scheduler scheduler = mock(Scheduler.class);
mockJobs(scheduler, jobOne, jobTwo, jobThree);
mockTriggers(scheduler, triggerOne, triggerTwo, triggerThree);
return scheduler;
}
@Bean
QuartzEndpoint endpoint(Scheduler scheduler) {
return new QuartzEndpoint(scheduler);
}
@Bean
QuartzEndpointWebExtension quartzEndpointWebExtension(QuartzEndpoint endpoint) {
return new QuartzEndpointWebExtension(endpoint);
}
private void mockJobs(Scheduler scheduler, JobDetail... jobs) throws SchedulerException {
MultiValueMap<String, JobKey> jobKeys = new LinkedMultiValueMap<>();
for (JobDetail jobDetail : jobs) {
JobKey key = jobDetail.getKey();
given(scheduler.getJobDetail(key)).willReturn(jobDetail);
jobKeys.add(key.getGroup(), key);
}
given(scheduler.getJobGroupNames()).willReturn(new ArrayList<>(jobKeys.keySet()));
for (Entry<String, List<JobKey>> entry : jobKeys.entrySet()) {
given(scheduler.getJobKeys(GroupMatcher.jobGroupEquals(entry.getKey())))
.willReturn(new LinkedHashSet<>(entry.getValue()));
}
}
void mockTriggers(Scheduler scheduler, Trigger... triggers) throws SchedulerException {
MultiValueMap<String, TriggerKey> triggerKeys = new LinkedMultiValueMap<>();
for (Trigger trigger : triggers) {
TriggerKey key = trigger.getKey();
given(scheduler.getTrigger(key)).willReturn(trigger);
given(scheduler.getTriggerState(key)).willReturn(TriggerState.NORMAL);
triggerKeys.add(key.getGroup(), key);
}
given(scheduler.getTriggerGroupNames()).willReturn(new ArrayList<>(triggerKeys.keySet()));
for (Entry<String, List<TriggerKey>> entry : triggerKeys.entrySet()) {
given(scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals(entry.getKey())))
.willReturn(new LinkedHashSet<>(entry.getValue()));
}
}
}
}

@ -112,6 +112,9 @@ The following technology-agnostic endpoints are available:
| `mappings` | `mappings`
| Displays a collated list of all `@RequestMapping` paths. | Displays a collated list of all `@RequestMapping` paths.
|`quartz`
|Shows information about Quartz Scheduler jobs.
| `scheduledtasks` | `scheduledtasks`
| Displays the scheduled tasks in your application. | Displays the scheduled tasks in your application.
@ -272,6 +275,10 @@ The following table shows the default exposure for the built-in endpoints:
| N/A | N/A
| No | No
| `quartz`
| Yes
| No
| `scheduledtasks` | `scheduledtasks`
| Yes | Yes
| No | No

@ -6,8 +6,10 @@ plugins {
description = "Spring Boot Quartz smoke test" description = "Spring Boot Quartz smoke test"
dependencies { dependencies {
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jdbc")) implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-actuator"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-quartz")) implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-quartz"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jdbc"))
runtimeOnly("com.h2database:h2") runtimeOnly("com.h2database:h2")

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,9 +16,16 @@
package smoketest.quartz; package smoketest.quartz;
import java.util.Calendar;
import org.quartz.CalendarIntervalScheduleBuilder;
import org.quartz.CronScheduleBuilder;
import org.quartz.DailyTimeIntervalScheduleBuilder;
import org.quartz.DateBuilder.IntervalUnit;
import org.quartz.JobBuilder; import org.quartz.JobBuilder;
import org.quartz.JobDetail; import org.quartz.JobDetail;
import org.quartz.SimpleScheduleBuilder; import org.quartz.SimpleScheduleBuilder;
import org.quartz.TimeOfDay;
import org.quartz.Trigger; import org.quartz.Trigger;
import org.quartz.TriggerBuilder; import org.quartz.TriggerBuilder;
@ -34,18 +41,50 @@ public class SampleQuartzApplication {
} }
@Bean @Bean
public JobDetail sampleJobDetail() { public JobDetail helloJobDetail() {
return JobBuilder.newJob(SampleJob.class).withIdentity("sampleJob").usingJobData("name", "World").storeDurably() return JobBuilder.newJob(SampleJob.class).withIdentity("helloJob", "samples").usingJobData("name", "World")
.build(); .storeDurably().build();
}
@Bean
public JobDetail anotherJobDetail() {
return JobBuilder.newJob(SampleJob.class).withIdentity("anotherJob", "samples").usingJobData("name", "Everyone")
.storeDurably().build();
}
@Bean
public Trigger everyTwoSecTrigger() {
return TriggerBuilder.newTrigger().forJob("helloJob", "samples").withIdentity("sampleTrigger")
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2).repeatForever()).build();
}
@Bean
public Trigger everyDayTrigger() {
return TriggerBuilder.newTrigger().forJob("helloJob", "samples").withIdentity("every-day", "samples")
.withSchedule(SimpleScheduleBuilder.repeatHourlyForever(24)).build();
} }
@Bean @Bean
public Trigger sampleJobTrigger() { public Trigger threeAmWeekdaysTrigger() {
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2) return TriggerBuilder.newTrigger().forJob("anotherJob", "samples").withIdentity("3am-weekdays", "samples")
.repeatForever(); .withSchedule(CronScheduleBuilder.atHourAndMinuteOnGivenDaysOfWeek(3, 0, 1, 2, 3, 4, 5)).build();
}
return TriggerBuilder.newTrigger().forJob(sampleJobDetail()).withIdentity("sampleTrigger") @Bean
.withSchedule(scheduleBuilder).build(); public Trigger onceAWeekTrigger() {
return TriggerBuilder.newTrigger().forJob("anotherJob", "samples").withIdentity("once-a-week", "samples")
.withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withIntervalInWeeks(1))
.build();
}
@Bean
public Trigger everyHourWorkingHourTuesdayAndThursdayTrigger() {
return TriggerBuilder.newTrigger().forJob("helloJob", "samples").withIdentity("every-hour-tue-thu", "samples")
.withSchedule(DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()
.onDaysOfTheWeek(Calendar.TUESDAY, Calendar.THURSDAY)
.startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0))
.endingDailyAt(TimeOfDay.hourAndMinuteOfDay(18, 0)).withInterval(1, IntervalUnit.HOUR))
.build();
} }
} }

@ -1 +1,3 @@
spring.quartz.job-store-type=jdbc spring.quartz.job-store-type=jdbc
management.endpoints.web.exposure.include=health,quartz

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -38,8 +38,9 @@ import static org.hamcrest.Matchers.containsString;
class SampleQuartzApplicationTests { class SampleQuartzApplicationTests {
@Test @Test
void quartzJobIsTriggered(CapturedOutput output) throws InterruptedException { void quartzJobIsTriggered(CapturedOutput output) {
try (ConfigurableApplicationContext context = SpringApplication.run(SampleQuartzApplication.class)) { try (ConfigurableApplicationContext context = SpringApplication.run(SampleQuartzApplication.class,
"--server.port=0")) {
Awaitility.waitAtMost(Duration.ofSeconds(5)).until(output::toString, containsString("Hello World!")); Awaitility.waitAtMost(Duration.ofSeconds(5)).until(output::toString, containsString("Hello World!"));
} }
} }

@ -0,0 +1,110 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.quartz;
import java.util.Map;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.assertj.core.api.InstanceOfAssertFactory;
import org.assertj.core.api.MapAssert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
/**
* Web tests for {@link SampleQuartzApplication}.
*
* @author Stephane Nicoll
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class SampleQuartzApplicationWebTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void quartzGroupNames() {
Map<String, Object> content = getContent("/actuator/quartz");
assertThat(content).containsOnlyKeys("jobs", "triggers");
}
@Test
void quartzJobGroups() {
Map<String, Object> content = getContent("/actuator/quartz/jobs");
assertThat(content).containsOnlyKeys("groups");
assertThat(content).extractingByKey("groups", nestedMap()).containsOnlyKeys("samples");
}
@Test
void quartzTriggerGroups() {
Map<String, Object> content = getContent("/actuator/quartz/triggers");
assertThat(content).containsOnlyKeys("groups");
assertThat(content).extractingByKey("groups", nestedMap()).containsOnlyKeys("DEFAULT", "samples");
}
@Test
void quartzJobDetail() {
Map<String, Object> content = getContent("/actuator/quartz/jobs/samples/helloJob");
assertThat(content).containsEntry("name", "helloJob").containsEntry("group", "samples");
}
@Test
void quartzJobDetailWhenNameDoesNotExistReturns404() {
ResponseEntity<String> response = this.restTemplate.getForEntity("/actuator/quartz/jobs/samples/does-not-exist",
String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
void quartzTriggerDetail() {
Map<String, Object> content = getContent("/actuator/quartz/triggers/samples/3am-weekdays");
assertThat(content).contains(entry("group", "samples"), entry("name", "3am-weekdays"), entry("state", "NORMAL"),
entry("type", "cron"));
}
@Test
void quartzTriggerDetailWhenNameDoesNotExistReturns404() {
ResponseEntity<String> response = this.restTemplate
.getForEntity("/actuator/quartz/triggers/samples/does-not-exist", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
private Map<String, Object> getContent(String path) {
ResponseEntity<Map<String, Object>> entity = asMapEntity(this.restTemplate.getForEntity(path, Map.class));
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
return entity.getBody();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static <K, V> ResponseEntity<Map<K, V>> asMapEntity(ResponseEntity<Map> entity) {
return (ResponseEntity) entity;
}
@SuppressWarnings("rawtypes")
private static InstanceOfAssertFactory<Map, MapAssert<String, Object>> nestedMap() {
return InstanceOfAssertFactories.map(String.class, Object.class);
}
}
Loading…
Cancel
Save