Move code from spring-boot-actuator to spring-boot-quartz
This commit is contained in:
committed by
Phillip Webb
parent
81771ecb24
commit
aea09106f2
@@ -0,0 +1,862 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.quartz.actuate.endpoint;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
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 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.quartz.utils.Key;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
|
||||
import org.springframework.boot.actuate.endpoint.SanitizableData;
|
||||
import org.springframework.boot.actuate.endpoint.Sanitizer;
|
||||
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
|
||||
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 4.0.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;
|
||||
|
||||
public QuartzEndpoint(Scheduler scheduler, Iterable<SanitizingFunction> sanitizingFunctions) {
|
||||
Assert.notNull(scheduler, "'scheduler' must not be null");
|
||||
this.scheduler = scheduler;
|
||||
this.sanitizer = new Sanitizer(sanitizingFunctions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 QuartzDescriptor quartzReport() throws SchedulerException {
|
||||
return new QuartzDescriptor(new GroupNamesDescriptor(this.scheduler.getJobGroupNames()),
|
||||
new GroupNamesDescriptor(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 QuartzGroupsDescriptor 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::getName)
|
||||
.toList();
|
||||
result.put(groupName, Collections.singletonMap("jobs", jobs));
|
||||
}
|
||||
return new QuartzGroupsDescriptor(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 QuartzGroupsDescriptor 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::getName)
|
||||
.toList());
|
||||
result.put(groupName, groupDetails);
|
||||
}
|
||||
return new QuartzGroupsDescriptor(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 QuartzJobGroupSummaryDescriptor quartzJobGroupSummary(String group) throws SchedulerException {
|
||||
List<JobDetail> jobs = findJobsByGroup(group);
|
||||
if (jobs.isEmpty() && !this.scheduler.getJobGroupNames().contains(group)) {
|
||||
return null;
|
||||
}
|
||||
Map<String, QuartzJobSummaryDescriptor> result = new LinkedHashMap<>();
|
||||
for (JobDetail job : jobs) {
|
||||
result.put(job.getKey().getName(), QuartzJobSummaryDescriptor.of(job));
|
||||
}
|
||||
return new QuartzJobGroupSummaryDescriptor(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 QuartzTriggerGroupSummaryDescriptor 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) -> {
|
||||
TriggerDescriptor triggerDescriptor = TriggerDescriptor.of(trigger);
|
||||
Map<String, Object> triggerTypes = result.computeIfAbsent(triggerDescriptor.getType(),
|
||||
(key) -> new LinkedHashMap<>());
|
||||
triggerTypes.put(trigger.getKey().getName(), triggerDescriptor.buildSummary(true));
|
||||
});
|
||||
boolean paused = this.scheduler.getPausedTriggerGroups().contains(group);
|
||||
return new QuartzTriggerGroupSummaryDescriptor(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 QuartzJobDetailsDescriptor 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
|
||||
* @param showUnsanitized whether to sanitize values in data map
|
||||
* @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 QuartzJobDetailsDescriptor quartzJob(String groupName, String jobName, boolean showUnsanitized)
|
||||
throws SchedulerException {
|
||||
JobKey jobKey = JobKey.jobKey(jobName, groupName);
|
||||
JobDetail jobDetail = this.scheduler.getJobDetail(jobKey);
|
||||
if (jobDetail == null) {
|
||||
return null;
|
||||
}
|
||||
List<? extends Trigger> triggers = this.scheduler.getTriggersOfJob(jobKey);
|
||||
return new QuartzJobDetailsDescriptor(jobDetail, sanitizeJobDataMap(jobDetail.getJobDataMap(), showUnsanitized),
|
||||
extractTriggersSummary(triggers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers (execute it now) a Quartz job by its group and job name.
|
||||
* @param groupName the name of the job's group
|
||||
* @param jobName the name of the job
|
||||
* @return a description of the triggered job or {@code null} if the job does not
|
||||
* exist
|
||||
* @throws SchedulerException if there is an error triggering the job
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public QuartzJobTriggerDescriptor triggerQuartzJob(String groupName, String jobName) throws SchedulerException {
|
||||
return triggerQuartzJob(JobKey.jobKey(jobName, groupName));
|
||||
}
|
||||
|
||||
private QuartzJobTriggerDescriptor triggerQuartzJob(JobKey jobKey) throws SchedulerException {
|
||||
JobDetail jobDetail = this.scheduler.getJobDetail(jobKey);
|
||||
if (jobDetail == null) {
|
||||
return null;
|
||||
}
|
||||
this.scheduler.triggerJob(jobKey);
|
||||
return new QuartzJobTriggerDescriptor(jobDetail);
|
||||
}
|
||||
|
||||
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(TriggerDescriptor.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
|
||||
* @param showUnsanitized whether to sanitize values in data map
|
||||
* @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
|
||||
*/
|
||||
Map<String, Object> quartzTrigger(String groupName, String triggerName, boolean showUnsanitized)
|
||||
throws SchedulerException {
|
||||
TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, groupName);
|
||||
Trigger trigger = this.scheduler.getTrigger(triggerKey);
|
||||
if (trigger == null) {
|
||||
return null;
|
||||
}
|
||||
TriggerState triggerState = this.scheduler.getTriggerState(triggerKey);
|
||||
TriggerDescriptor triggerDescriptor = TriggerDescriptor.of(trigger);
|
||||
Map<String, Object> jobDataMap = sanitizeJobDataMap(trigger.getJobDataMap(), showUnsanitized);
|
||||
return OperationResponseBody.of(triggerDescriptor.buildDetails(triggerState, jobDataMap));
|
||||
}
|
||||
|
||||
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, boolean showUnsanitized) {
|
||||
if (dataMap == null) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> map = new LinkedHashMap<>(dataMap.getWrappedMap());
|
||||
map.replaceAll((key, value) -> getSanitizedValue(showUnsanitized, key, value));
|
||||
return map;
|
||||
}
|
||||
|
||||
private Object getSanitizedValue(boolean showUnsanitized, String key, Object value) {
|
||||
SanitizableData data = new SanitizableData(null, key, value);
|
||||
return this.sanitizer.sanitize(data, showUnsanitized);
|
||||
}
|
||||
|
||||
private static TemporalUnit temporalUnit(IntervalUnit unit) {
|
||||
return switch (unit) {
|
||||
case DAY -> ChronoUnit.DAYS;
|
||||
case HOUR -> ChronoUnit.HOURS;
|
||||
case MINUTE -> ChronoUnit.MINUTES;
|
||||
case MONTH -> ChronoUnit.MONTHS;
|
||||
case SECOND -> ChronoUnit.SECONDS;
|
||||
case MILLISECOND -> ChronoUnit.MILLIS;
|
||||
case WEEK -> ChronoUnit.WEEKS;
|
||||
case YEAR -> ChronoUnit.YEARS;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of available job and trigger group names.
|
||||
*/
|
||||
public static final class QuartzDescriptor implements OperationResponseBody {
|
||||
|
||||
private final GroupNamesDescriptor jobs;
|
||||
|
||||
private final GroupNamesDescriptor triggers;
|
||||
|
||||
QuartzDescriptor(GroupNamesDescriptor jobs, GroupNamesDescriptor triggers) {
|
||||
this.jobs = jobs;
|
||||
this.triggers = triggers;
|
||||
}
|
||||
|
||||
public GroupNamesDescriptor getJobs() {
|
||||
return this.jobs;
|
||||
}
|
||||
|
||||
public GroupNamesDescriptor getTriggers() {
|
||||
return this.triggers;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of group names.
|
||||
*/
|
||||
public static class GroupNamesDescriptor {
|
||||
|
||||
private final Set<String> groups;
|
||||
|
||||
public GroupNamesDescriptor(List<String> groups) {
|
||||
this.groups = new LinkedHashSet<>(groups);
|
||||
}
|
||||
|
||||
public Set<String> getGroups() {
|
||||
return this.groups;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of each group identified by name.
|
||||
*/
|
||||
public static class QuartzGroupsDescriptor implements OperationResponseBody {
|
||||
|
||||
private final Map<String, Object> groups;
|
||||
|
||||
public QuartzGroupsDescriptor(Map<String, Object> groups) {
|
||||
this.groups = groups;
|
||||
}
|
||||
|
||||
public Map<String, Object> getGroups() {
|
||||
return this.groups;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of the {@link JobDetail jobs} in a given group.
|
||||
*/
|
||||
public static final class QuartzJobGroupSummaryDescriptor implements OperationResponseBody {
|
||||
|
||||
private final String group;
|
||||
|
||||
private final Map<String, QuartzJobSummaryDescriptor> jobs;
|
||||
|
||||
QuartzJobGroupSummaryDescriptor(String group, Map<String, QuartzJobSummaryDescriptor> jobs) {
|
||||
this.group = group;
|
||||
this.jobs = jobs;
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return this.group;
|
||||
}
|
||||
|
||||
public Map<String, QuartzJobSummaryDescriptor> getJobs() {
|
||||
return this.jobs;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of a {@link Job Quartz Job}.
|
||||
*/
|
||||
public static final class QuartzJobSummaryDescriptor {
|
||||
|
||||
private final String className;
|
||||
|
||||
QuartzJobSummaryDescriptor(JobDetail job) {
|
||||
this.className = job.getJobClass().getName();
|
||||
}
|
||||
|
||||
private static QuartzJobSummaryDescriptor of(JobDetail job) {
|
||||
return new QuartzJobSummaryDescriptor(job);
|
||||
}
|
||||
|
||||
public String getClassName() {
|
||||
return this.className;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of a triggered on-demand {@link Job Quartz Job}.
|
||||
*
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public static final class QuartzJobTriggerDescriptor {
|
||||
|
||||
private final String group;
|
||||
|
||||
private final String name;
|
||||
|
||||
private final String className;
|
||||
|
||||
private final Instant triggerTime;
|
||||
|
||||
QuartzJobTriggerDescriptor(JobDetail jobDetail) {
|
||||
this.group = jobDetail.getKey().getGroup();
|
||||
this.name = jobDetail.getKey().getName();
|
||||
this.className = jobDetail.getJobClass().getName();
|
||||
this.triggerTime = Instant.now();
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return this.group;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public String getClassName() {
|
||||
return this.className;
|
||||
}
|
||||
|
||||
public Instant getTriggerTime() {
|
||||
return this.triggerTime;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of a {@link Job Quartz Job}.
|
||||
*/
|
||||
public static final class QuartzJobDetailsDescriptor implements OperationResponseBody {
|
||||
|
||||
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;
|
||||
|
||||
QuartzJobDetailsDescriptor(JobDetail jobDetail, Map<String, Object> data, List<Map<String, Object>> triggers) {
|
||||
this.group = jobDetail.getKey().getGroup();
|
||||
this.name = jobDetail.getKey().getName();
|
||||
this.description = jobDetail.getDescription();
|
||||
this.className = jobDetail.getJobClass().getName();
|
||||
this.durable = jobDetail.isDurable();
|
||||
this.requestRecovery = jobDetail.requestsRecovery();
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of the {@link Trigger triggers} in a given group.
|
||||
*/
|
||||
public static final class QuartzTriggerGroupSummaryDescriptor implements OperationResponseBody {
|
||||
|
||||
private final String group;
|
||||
|
||||
private final boolean paused;
|
||||
|
||||
private final Triggers triggers;
|
||||
|
||||
QuartzTriggerGroupSummaryDescriptor(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;
|
||||
|
||||
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 TriggerDescriptor {
|
||||
|
||||
private static final Map<Class<? extends Trigger>, Function<Trigger, TriggerDescriptor>> DESCRIBERS;
|
||||
|
||||
static {
|
||||
Map<Class<? extends Trigger>, Function<Trigger, TriggerDescriptor>> descriptors = new LinkedHashMap<>();
|
||||
descriptors.put(CronTrigger.class, (trigger) -> new CronTriggerDescriptor((CronTrigger) trigger));
|
||||
descriptors.put(SimpleTrigger.class, (trigger) -> new SimpleTriggerDescriptor((SimpleTrigger) trigger));
|
||||
descriptors.put(DailyTimeIntervalTrigger.class,
|
||||
(trigger) -> new DailyTimeIntervalTriggerDescriptor((DailyTimeIntervalTrigger) trigger));
|
||||
descriptors.put(CalendarIntervalTrigger.class,
|
||||
(trigger) -> new CalendarIntervalTriggerDescriptor((CalendarIntervalTrigger) trigger));
|
||||
DESCRIBERS = Map.copyOf(descriptors);
|
||||
}
|
||||
|
||||
private final Trigger trigger;
|
||||
|
||||
private final TriggerType type;
|
||||
|
||||
protected TriggerDescriptor(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;
|
||||
}
|
||||
|
||||
static TriggerDescriptor of(Trigger trigger) {
|
||||
return DESCRIBERS.entrySet()
|
||||
.stream()
|
||||
.filter((entry) -> entry.getKey().isInstance(trigger))
|
||||
.map((entry) -> entry.getValue().apply(trigger))
|
||||
.findFirst()
|
||||
.orElse(new CustomTriggerDescriptor(trigger));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of a {@link CronTrigger}.
|
||||
*/
|
||||
public static final class CronTriggerDescriptor extends TriggerDescriptor {
|
||||
|
||||
private final CronTrigger trigger;
|
||||
|
||||
public CronTriggerDescriptor(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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of a {@link SimpleTrigger}.
|
||||
*/
|
||||
public static final class SimpleTriggerDescriptor extends TriggerDescriptor {
|
||||
|
||||
private final SimpleTrigger trigger;
|
||||
|
||||
public SimpleTriggerDescriptor(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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of a {@link DailyTimeIntervalTrigger}.
|
||||
*/
|
||||
public static final class DailyTimeIntervalTriggerDescriptor extends TriggerDescriptor {
|
||||
|
||||
private final DailyTimeIntervalTrigger trigger;
|
||||
|
||||
public DailyTimeIntervalTriggerDescriptor(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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of a {@link CalendarIntervalTrigger}.
|
||||
*/
|
||||
public static final class CalendarIntervalTriggerDescriptor extends TriggerDescriptor {
|
||||
|
||||
private final CalendarIntervalTrigger trigger;
|
||||
|
||||
public CalendarIntervalTriggerDescriptor(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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of a custom {@link Trigger}.
|
||||
*/
|
||||
public static final class CustomTriggerDescriptor extends TriggerDescriptor {
|
||||
|
||||
public CustomTriggerDescriptor(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,138 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.quartz.actuate.endpoint;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.quartz.SchedulerException;
|
||||
|
||||
import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.Show;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Selector;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
|
||||
import org.springframework.boot.quartz.actuate.endpoint.QuartzEndpoint.QuartzGroupsDescriptor;
|
||||
import org.springframework.boot.quartz.actuate.endpoint.QuartzEndpoint.QuartzJobDetailsDescriptor;
|
||||
import org.springframework.boot.quartz.actuate.endpoint.QuartzEndpoint.QuartzJobGroupSummaryDescriptor;
|
||||
import org.springframework.boot.quartz.actuate.endpoint.QuartzEndpoint.QuartzTriggerGroupSummaryDescriptor;
|
||||
import org.springframework.boot.quartz.actuate.endpoint.QuartzEndpointWebExtension.QuartzEndpointWebExtensionRuntimeHints;
|
||||
import org.springframework.context.annotation.ImportRuntimeHints;
|
||||
|
||||
/**
|
||||
* {@link EndpointWebExtension @EndpointWebExtension} for the {@link QuartzEndpoint}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@EndpointWebExtension(endpoint = QuartzEndpoint.class)
|
||||
@ImportRuntimeHints(QuartzEndpointWebExtensionRuntimeHints.class)
|
||||
public class QuartzEndpointWebExtension {
|
||||
|
||||
private final QuartzEndpoint delegate;
|
||||
|
||||
private final Show showValues;
|
||||
|
||||
private final Set<String> roles;
|
||||
|
||||
public QuartzEndpointWebExtension(QuartzEndpoint delegate, Show showValues, Set<String> roles) {
|
||||
this.delegate = delegate;
|
||||
this.showValues = showValues;
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public WebEndpointResponse<QuartzGroupsDescriptor> 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(SecurityContext securityContext,
|
||||
@Selector String jobsOrTriggers, @Selector String group, @Selector String name) throws SchedulerException {
|
||||
boolean showUnsanitized = this.showValues.isShown(securityContext, this.roles);
|
||||
return handle(jobsOrTriggers, () -> this.delegate.quartzJob(group, name, showUnsanitized),
|
||||
() -> this.delegate.quartzTrigger(group, name, showUnsanitized));
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a Quartz job.
|
||||
* @param jobs path segment "jobs"
|
||||
* @param group job's group
|
||||
* @param name job name
|
||||
* @param state desired state
|
||||
* @return web endpoint response
|
||||
* @throws SchedulerException if there is an error triggering the job
|
||||
* @since 3.5.0
|
||||
*/
|
||||
@WriteOperation
|
||||
public WebEndpointResponse<Object> triggerQuartzJob(@Selector String jobs, @Selector String group,
|
||||
@Selector String name, String state) throws SchedulerException {
|
||||
if ("jobs".equals(jobs) && "running".equals(state)) {
|
||||
return handleNull(this.delegate.triggerQuartzJob(group, name));
|
||||
}
|
||||
return new WebEndpointResponse<>(WebEndpointResponse.STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
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) {
|
||||
return (value != null) ? new WebEndpointResponse<>(value)
|
||||
: new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface ResponseSupplier<T> {
|
||||
|
||||
T get() throws SchedulerException;
|
||||
|
||||
}
|
||||
|
||||
static class QuartzEndpointWebExtensionRuntimeHints implements RuntimeHintsRegistrar {
|
||||
|
||||
private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar();
|
||||
|
||||
@Override
|
||||
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
|
||||
this.bindingRegistrar.registerReflectionHints(hints.reflection(), QuartzGroupsDescriptor.class,
|
||||
QuartzJobDetailsDescriptor.class, QuartzJobGroupSummaryDescriptor.class,
|
||||
QuartzTriggerGroupSummaryDescriptor.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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 endpoint for Quartz Scheduler.
|
||||
*/
|
||||
package org.springframework.boot.quartz.actuate.endpoint;
|
||||
@@ -0,0 +1,820 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.quartz.actuate.endpoint;
|
||||
|
||||
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.quartz.actuate.endpoint.QuartzEndpoint.QuartzDescriptor;
|
||||
import org.springframework.boot.quartz.actuate.endpoint.QuartzEndpoint.QuartzJobDetailsDescriptor;
|
||||
import org.springframework.boot.quartz.actuate.endpoint.QuartzEndpoint.QuartzJobGroupSummaryDescriptor;
|
||||
import org.springframework.boot.quartz.actuate.endpoint.QuartzEndpoint.QuartzJobSummaryDescriptor;
|
||||
import org.springframework.boot.quartz.actuate.endpoint.QuartzEndpoint.QuartzJobTriggerDescriptor;
|
||||
import org.springframework.boot.quartz.actuate.endpoint.QuartzEndpoint.QuartzTriggerGroupSummaryDescriptor;
|
||||
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.assertj.core.api.Assertions.within;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
||||
/**
|
||||
* 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, Collections.emptyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzReport() throws SchedulerException {
|
||||
given(this.scheduler.getJobGroupNames()).willReturn(Arrays.asList("jobSamples", "DEFAULT"));
|
||||
given(this.scheduler.getTriggerGroupNames()).willReturn(Collections.singletonList("triggerSamples"));
|
||||
QuartzDescriptor quartzReport = this.endpoint.quartzReport();
|
||||
assertThat(quartzReport.getJobs().getGroups()).containsOnly("jobSamples", "DEFAULT");
|
||||
assertThat(quartzReport.getTriggers().getGroups()).containsOnly("triggerSamples");
|
||||
then(this.scheduler).should().getJobGroupNames();
|
||||
then(this.scheduler).should().getTriggerGroupNames();
|
||||
then(this.scheduler).shouldHaveNoMoreInteractions();
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzReportWithNoJob() throws SchedulerException {
|
||||
given(this.scheduler.getJobGroupNames()).willReturn(Collections.emptyList());
|
||||
given(this.scheduler.getTriggerGroupNames()).willReturn(Arrays.asList("triggerSamples", "DEFAULT"));
|
||||
QuartzDescriptor 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());
|
||||
QuartzDescriptor 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"));
|
||||
QuartzJobGroupSummaryDescriptor 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());
|
||||
QuartzJobGroupSummaryDescriptor 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);
|
||||
QuartzJobGroupSummaryDescriptor summary = this.endpoint.quartzJobGroupSummary("DEFAULT");
|
||||
assertThat(summary).isNotNull();
|
||||
assertThat(summary.getGroup()).isEqualTo("DEFAULT");
|
||||
Map<String, QuartzJobSummaryDescriptor> 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"));
|
||||
QuartzTriggerGroupSummaryDescriptor 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());
|
||||
QuartzTriggerGroupSummaryDescriptor 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);
|
||||
QuartzTriggerGroupSummaryDescriptor 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);
|
||||
QuartzTriggerGroupSummaryDescriptor 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);
|
||||
QuartzTriggerGroupSummaryDescriptor 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);
|
||||
QuartzTriggerGroupSummaryDescriptor 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);
|
||||
QuartzTriggerGroupSummaryDescriptor 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);
|
||||
QuartzTriggerGroupSummaryDescriptor 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);
|
||||
QuartzTriggerGroupSummaryDescriptor 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);
|
||||
QuartzTriggerGroupSummaryDescriptor 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);
|
||||
QuartzTriggerGroupSummaryDescriptor 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);
|
||||
QuartzTriggerGroupSummaryDescriptor 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", true);
|
||||
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", true);
|
||||
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", true);
|
||||
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", true);
|
||||
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", true);
|
||||
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", true);
|
||||
assertThat(triggerDetails).extractingByKey("data", nestedMap())
|
||||
.containsOnly(entry("user", "user"), entry("password", "secret"),
|
||||
entry("url", "https://user:secret@example.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzTriggerWithDataMapAndShowUnsanitizedFalse() 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", false);
|
||||
assertThat(triggerDetails).extractingByKey("data", nestedMap())
|
||||
.containsOnly(entry("user", "******"), entry("password", "******"), entry("url", "******"));
|
||||
}
|
||||
|
||||
@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", true);
|
||||
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);
|
||||
QuartzJobDetailsDescriptor jobDetails = this.endpoint.quartzJob("samples", "hello", true);
|
||||
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));
|
||||
QuartzJobDetailsDescriptor jobDetails = this.endpoint.quartzJob("samples", "hello", true);
|
||||
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));
|
||||
QuartzJobDetailsDescriptor jobDetails = this.endpoint.quartzJob("samples", "hello", true);
|
||||
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));
|
||||
QuartzJobDetailsDescriptor jobDetails = this.endpoint.quartzJob("samples", "hello", true);
|
||||
assertThat(jobDetails.getTriggers()).hasSize(2);
|
||||
assertThat(jobDetails.getTriggers().get(0)).containsEntry("name", "two");
|
||||
assertThat(jobDetails.getTriggers().get(1)).containsEntry("name", "one");
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzJobWithDataMap() 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);
|
||||
QuartzJobDetailsDescriptor jobDetails = this.endpoint.quartzJob("samples", "hello", true);
|
||||
assertThat(jobDetails.getData()).containsOnly(entry("user", "user"), entry("password", "secret"),
|
||||
entry("url", "https://user:secret@example.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzJobWithDataMapAndShowUnsanitizedFalse() 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);
|
||||
QuartzJobDetailsDescriptor jobDetails = this.endpoint.quartzJob("samples", "hello", false);
|
||||
assertThat(jobDetails.getData()).containsOnly(entry("user", "******"), entry("password", "******"),
|
||||
entry("url", "******"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzJobShouldBeTriggered() throws SchedulerException {
|
||||
JobDetail job = JobBuilder.newJob(Job.class)
|
||||
.withIdentity("hello", "samples")
|
||||
.withDescription("A sample job")
|
||||
.storeDurably()
|
||||
.requestRecovery(false)
|
||||
.build();
|
||||
mockJobs(job);
|
||||
QuartzJobTriggerDescriptor quartzJobTriggerDescriptor = this.endpoint.triggerQuartzJob("samples", "hello");
|
||||
assertThat(quartzJobTriggerDescriptor).isNotNull();
|
||||
assertThat(quartzJobTriggerDescriptor.getName()).isEqualTo("hello");
|
||||
assertThat(quartzJobTriggerDescriptor.getGroup()).isEqualTo("samples");
|
||||
assertThat(quartzJobTriggerDescriptor.getClassName()).isEqualTo("org.quartz.Job");
|
||||
assertThat(quartzJobTriggerDescriptor.getTriggerTime()).isCloseTo(Instant.now(), within(5, ChronoUnit.SECONDS));
|
||||
then(this.scheduler).should().triggerJob(new JobKey("hello", "samples"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzJobShouldNotBeTriggeredWhenJobDoesNotExist() throws SchedulerException {
|
||||
QuartzJobTriggerDescriptor quartzJobTriggerDescriptor = this.endpoint.triggerQuartzJob("samples", "hello");
|
||||
assertThat(quartzJobTriggerDescriptor).isNull();
|
||||
then(this.scheduler).should(never()).triggerJob(any());
|
||||
}
|
||||
|
||||
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,112 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.quartz.actuate.endpoint;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.Show;
|
||||
import org.springframework.boot.quartz.actuate.endpoint.QuartzEndpoint.QuartzGroupsDescriptor;
|
||||
import org.springframework.boot.quartz.actuate.endpoint.QuartzEndpoint.QuartzJobDetailsDescriptor;
|
||||
import org.springframework.boot.quartz.actuate.endpoint.QuartzEndpoint.QuartzJobGroupSummaryDescriptor;
|
||||
import org.springframework.boot.quartz.actuate.endpoint.QuartzEndpoint.QuartzTriggerGroupSummaryDescriptor;
|
||||
import org.springframework.boot.quartz.actuate.endpoint.QuartzEndpointWebExtension.QuartzEndpointWebExtensionRuntimeHints;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link QuartzEndpointWebExtension}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
class QuartzEndpointWebExtensionTests {
|
||||
|
||||
private QuartzEndpointWebExtension webExtension;
|
||||
|
||||
private QuartzEndpoint delegate;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.delegate = mock(QuartzEndpoint.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenShowValuesIsNever() throws Exception {
|
||||
this.webExtension = new QuartzEndpointWebExtension(this.delegate, Show.NEVER, Collections.emptySet());
|
||||
this.webExtension.quartzJobOrTrigger(null, "jobs", "a", "b");
|
||||
this.webExtension.quartzJobOrTrigger(null, "triggers", "a", "b");
|
||||
then(this.delegate).should().quartzJob("a", "b", false);
|
||||
then(this.delegate).should().quartzTrigger("a", "b", false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenShowValuesIsAlways() throws Exception {
|
||||
this.webExtension = new QuartzEndpointWebExtension(this.delegate, Show.ALWAYS, Collections.emptySet());
|
||||
this.webExtension.quartzJobOrTrigger(null, "a", "b", "c");
|
||||
this.webExtension.quartzJobOrTrigger(null, "jobs", "a", "b");
|
||||
this.webExtension.quartzJobOrTrigger(null, "triggers", "a", "b");
|
||||
then(this.delegate).should().quartzJob("a", "b", true);
|
||||
then(this.delegate).should().quartzTrigger("a", "b", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenShowValuesIsWhenAuthorizedAndSecurityContextIsAuthorized() throws Exception {
|
||||
SecurityContext securityContext = mock(SecurityContext.class);
|
||||
given(securityContext.getPrincipal()).willReturn(mock(Principal.class));
|
||||
this.webExtension = new QuartzEndpointWebExtension(this.delegate, Show.WHEN_AUTHORIZED, Collections.emptySet());
|
||||
this.webExtension.quartzJobOrTrigger(securityContext, "jobs", "a", "b");
|
||||
this.webExtension.quartzJobOrTrigger(securityContext, "triggers", "a", "b");
|
||||
then(this.delegate).should().quartzJob("a", "b", true);
|
||||
then(this.delegate).should().quartzTrigger("a", "b", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenShowValuesIsWhenAuthorizedAndSecurityContextIsNotAuthorized() throws Exception {
|
||||
SecurityContext securityContext = mock(SecurityContext.class);
|
||||
this.webExtension = new QuartzEndpointWebExtension(this.delegate, Show.WHEN_AUTHORIZED, Collections.emptySet());
|
||||
this.webExtension.quartzJobOrTrigger(securityContext, "jobs", "a", "b");
|
||||
this.webExtension.quartzJobOrTrigger(securityContext, "triggers", "a", "b");
|
||||
then(this.delegate).should().quartzJob("a", "b", false);
|
||||
then(this.delegate).should().quartzTrigger("a", "b", false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRegisterHints() {
|
||||
RuntimeHints runtimeHints = new RuntimeHints();
|
||||
new QuartzEndpointWebExtensionRuntimeHints().registerHints(runtimeHints, getClass().getClassLoader());
|
||||
Set<Class<?>> bindingTypes = Set.of(QuartzGroupsDescriptor.class, QuartzJobDetailsDescriptor.class,
|
||||
QuartzJobGroupSummaryDescriptor.class, QuartzTriggerGroupSummaryDescriptor.class);
|
||||
for (Class<?> bindingType : bindingTypes) {
|
||||
assertThat(RuntimeHintsPredicates.reflection()
|
||||
.onType(bindingType)
|
||||
.withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(runtimeHints);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.quartz.actuate.endpoint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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.Show;
|
||||
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
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-else").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();
|
||||
}
|
||||
|
||||
@WebEndpointTest
|
||||
void quartzTriggerJob(WebTestClient client) {
|
||||
client.post()
|
||||
.uri("/actuator/quartz/jobs/samples/jobOne")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(Map.of("state", "running"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody()
|
||||
.jsonPath("group")
|
||||
.isEqualTo("samples")
|
||||
.jsonPath("name")
|
||||
.isEqualTo("jobOne")
|
||||
.jsonPath("className")
|
||||
.isEqualTo("org.quartz.Job")
|
||||
.jsonPath("triggerTime")
|
||||
.isNotEmpty();
|
||||
}
|
||||
|
||||
@WebEndpointTest
|
||||
void quartzTriggerJobWithUnknownJobKey(WebTestClient client) {
|
||||
client.post()
|
||||
.uri("/actuator/quartz/jobs/samples/does-not-exist")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(Map.of("state", "running"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isNotFound();
|
||||
}
|
||||
|
||||
@WebEndpointTest
|
||||
void quartzTriggerJobWithUnknownState(WebTestClient client) {
|
||||
client.post()
|
||||
.uri("/actuator/quartz/jobs/samples/jobOne")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(Map.of("state", "unknown"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isBadRequest();
|
||||
}
|
||||
|
||||
@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, Collections.emptyList());
|
||||
}
|
||||
|
||||
@Bean
|
||||
QuartzEndpointWebExtension quartzEndpointWebExtension(QuartzEndpoint endpoint) {
|
||||
return new QuartzEndpointWebExtension(endpoint, Show.ALWAYS, Collections.emptySet());
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user