Merge pull request #10364 from vpavic
* pr/10364: Polish "Add Quartz actuator endpoint" Add Quartz actuator endpoint Closes gh-10364pull/30708/head
commit
a07c8e6e18
@ -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[]
|
@ -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;
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1 +1,3 @@
|
|||||||
spring.quartz.job-store-type=jdbc
|
spring.quartz.job-store-type=jdbc
|
||||||
|
|
||||||
|
management.endpoints.web.exposure.include=health,quartz
|
@ -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…
Reference in New Issue