diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/src/main/asciidoc/endpoints/quartz.adoc b/src/main/asciidoc/endpoints/quartz.adoc new file mode 100644 index 0000000..b1c1f68 --- /dev/null +++ b/src/main/asciidoc/endpoints/quartz.adoc @@ -0,0 +1,113 @@ +[[quartz]] += Quartz + +Quartz actuator provides end points for managing jobs and triggers + +[[quartz-jobs]] +== Quartz Job (`quartz-jobs`) +The `quartz-jobs` endpoint provides access to application's quartz jobs + +[[list-job]] +=== Retrieving jobs +To retrieve jobs, make `GET` request to `/actuator/quartz-jobs` as shown in the following curl-based example: + +include::{snippets}/quartz-jobs/list/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/quartz-jobs/list/http-response.adoc[] + +[[list-job-query-parameter]] +==== Query parameter +Jobs listing can be further filtered using `group` and `name` query parameter.The following table shows the supported query parameters: + +[cols="2,4"] +include::{snippets}/quartz-jobs/list/request-parameters.adoc[] + +[[list-job-response-structure]] +==== Response Structure +The response contains the details of the jobs managed by quartz.The following table describes +the structure of the response: + +[cols="3,1,3"] +include::{snippets}/quartz-jobs/list/response-fields.adoc[] + +[[job-detail]] +=== Retrieving Job details +To retrieve job details with trigger information, make GET request to `/actuator/quartz-jobs/{group}/{name}` as shown in the following curl-based example: + +include::{snippets}/quartz-jobs/named/jobdetails/curl-request.adoc[] + +[[job-detail-response-structure]] +==== Response Structure +The response contains details about the job and its associated triggers. The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/quartz-jobs/named/jobdetails/response-fields.adoc[] + +[[pause-job]] +=== Pause Job +To pause particular job, make a `POST` request to `/actuator/quartz-jobs/{group}/{name}/pause` as shown in the following curl-based example: + +include::{snippets}/quartz-jobs/named/job/pause/curl-request.adoc[] + + +[[resume-job]] +=== Resume Job +To resume particular job, make a `POST` request to `/actuator/quartz-jobs/{group}/{name}/resume` as shown in the following curl-based example: + +include::{snippets}/quartz-jobs/named/job/resume/curl-request.adoc[] + +[[pause-jobgroup]] +=== Pause Job group +To pause particular job group, make `POST` request to `/actuator/quartz-jobs/{group}` as shown in the following curl-based example: + +include::{snippets}/quartz-jobs/named/jobgroup/pause/curl-request.adoc[] + +[[resume-jobgroup]] +=== Resume Job group +To resume particular job group, make `POST` request to `/actuator/quartz-jobs/{group}` as shown in the following curl-based example: + +include::{snippets}/quartz-jobs/named/jobgroup/resume/curl-request.adoc[] + +[[quartz-triggers]] +== Quartz Triggers (`quartz-triggers`) +The `quartz-triggers` endpoint provides access to application's quartz triggers + +[[list-triggers]] +=== Retrieving Triggers +To retrieve triggers, make `GET` request to `/actuator/quartz-triggers` as shown in the following curl-based example: + +include::{snippets}/quartz-jobs/list/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/quartz-triggers/list/http-response.adoc[] + +[[list-trigger-query-parameter]] +==== Query parameter +Triggers listing can be further filtered using `group` and `name` query parameter.The following table shows the supported query parameters: + +[cols="2,4"] +include::{snippets}/quartz-triggers/list/request-parameters.adoc[] + +[[list-trigger-response-structure]] +==== Response Structure +The response contains the details of the triggers managed by quartz.The following table describes +the structure of the response: + +[cols="3,1,3"] +include::{snippets}/quartz-triggers/list/response-fields.adoc[] + +[[pause-trigger]] +=== Pause Trigger +To pause particular trigger, make a `POST` request to `/actuator/quartz-triggers/{group}/{name}/pause` as shown in the following curl-based example: + +include::{snippets}/quartz-triggers/named/trigger/pause/curl-request.adoc[] + + +[[resume-trigger]] +=== Resume Trigger +To resume particular trigger, make a `POST` request to `/actuator/quartz-triggers/{group}/{name}/resume` as shown in the following curl-based example: + +include::{snippets}/quartz-triggers/named/trigger/resume/curl-request.adoc[] diff --git a/src/main/asciidoc/endpoints/quartz.adoc.md b/src/main/asciidoc/endpoints/quartz.adoc.md new file mode 100644 index 0000000..2d83705 --- /dev/null +++ b/src/main/asciidoc/endpoints/quartz.adoc.md @@ -0,0 +1,25 @@ +[[quartz]] = Quartz + +Quartz actuator provides end points for managing jobs and triggers + +[[quartz-jobs]] == Quartz Job (`quartz-jobs`) + +The `quartz-jobs` endpoint provides access to application's quartz jobs + +[[pause-job]] === Pause Job To pause particular job, make a `POST` +request to `/actuator/quartz-jobs/{group}/{name}/pause` as shown in the +following curl-based example: + +include::{snippets}/quartz-jobs/named/job/pause/curl-request.adoc[] + +[[resume-job]] === Resume Job To resume particular job, make a `POST` +request to `/actuator/quartz-jobs/{group}/{name}/resume` as shown in the +following curl-based example: + +include::{snippets}/quartz-jobs/named/job/resume/curl-request.adoc[] + +[[pause-jobgroup]] === Pause Job group To pause particular job group, +make `POST` request to `/actuator/quartz-jobs/{group}` as shown in the +following curl-based example: + +include::{snippets}/quartz-jobs/named/jobgroup/pause/curl-request.adoc[] diff --git a/src/main/asciidoc/endpoints/quartz.adoc.xml b/src/main/asciidoc/endpoints/quartz.adoc.xml new file mode 100644 index 0000000..80ba591 --- /dev/null +++ b/src/main/asciidoc/endpoints/quartz.adoc.xml @@ -0,0 +1,48 @@ + + +
+ + + + + [[quartz]] = Quartz + + + Quartz actuator provides end points for managing jobs and triggers + + + [[quartz-jobs]] == Quartz Job (quartz-jobs) + + + The quartz-jobs endpoint provides access to + application's quartz jobs + + + [[pause-job]] === Pause Job To pause particular job, make a + POST request to + /actuator/quartz-jobs/{group}/{name}/pause as shown + in the following curl-based example: + + + include::{snippets}/quartz-jobs/named/job/pause/curl-request.adoc[] + + + [[resume-job]] === Resume Job To resume particular job, make a + POST request to + /actuator/quartz-jobs/{group}/{name}/resume as + shown in the following curl-based example: + + + include::{snippets}/quartz-jobs/named/job/resume/curl-request.adoc[] + + + [[pause-jobgroup]] === Pause Job group To pause particular job group, + make POST request to + /actuator/quartz-jobs/{group} as shown in the + following curl-based example: + + + include::{snippets}/quartz-jobs/named/jobgroup/pause/curl-request.adoc[] + +
diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc new file mode 100644 index 0000000..94841f4 --- /dev/null +++ b/src/main/asciidoc/index.adoc @@ -0,0 +1,10 @@ +:doctype: book +:toc: left +:toclevels: 4 +:source-highlighter: prettify +:numbered: +:icons: font +:hide-uri-scheme: +:docinfo: shared,private + +include::endpoints/quartz.adoc[leveloffset=+1] diff --git a/src/main/java/org/sathyabodh/actuator/autoconfigure/cache/CachesEndPointAutoConfiguration.java b/src/main/java/org/sathyabodh/actuator/autoconfigure/cache/CachesEndPointAutoConfiguration.java new file mode 100644 index 0000000..29d8314 --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/autoconfigure/cache/CachesEndPointAutoConfiguration.java @@ -0,0 +1,34 @@ +package org.sathyabodh.actuator.autoconfigure.cache; + +import java.util.Map; + +import org.sathyabodh.actuator.cache.CachesEndPoint; +import org.sathyabodh.actuator.cache.CachesEndPointWebExtension; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnClass(CacheManager.class) +@AutoConfigureAfter(CacheAutoConfiguration.class) +public class CachesEndPointAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + public CachesEndPoint cachesEndPoint(Map cacheManagers){ + return new CachesEndPoint(cacheManagers); + } + + @Bean + @ConditionalOnBean(CachesEndPoint.class) + public CachesEndPointWebExtension cachesEndPointWebExtension(CachesEndPoint cachesEndPoint){ + return new CachesEndPointWebExtension(cachesEndPoint); + } +} diff --git a/src/main/java/org/sathyabodh/actuator/autoconfigure/quartz/QuartzTriggerEndPointAutoConfiguration.java b/src/main/java/org/sathyabodh/actuator/autoconfigure/quartz/QuartzTriggerEndPointAutoConfiguration.java new file mode 100644 index 0000000..442dc9c --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/autoconfigure/quartz/QuartzTriggerEndPointAutoConfiguration.java @@ -0,0 +1,33 @@ +package org.sathyabodh.actuator.autoconfigure.quartz; + +import org.quartz.Scheduler; +import org.quartz.SchedulerFactory; +import org.sathyabodh.actuator.quartz.QuartzTriggerEndPoint; +import org.sathyabodh.actuator.quartz.QuartzTriggerEndPointWebExtension; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnClass({Scheduler.class, SchedulerFactory.class}) +@AutoConfigureAfter(QuartzAutoConfiguration.class) +public class QuartzTriggerEndPointAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + public QuartzTriggerEndPoint quartzTriggerEndPoint(Scheduler scheduler){ + return new QuartzTriggerEndPoint(scheduler); + } + + @Bean + @ConditionalOnBean(QuartzTriggerEndPoint.class) + public QuartzTriggerEndPointWebExtension quartzTriggerEndPointWebExtension(QuartzTriggerEndPoint quartzTriggerEndPoint){ + return new QuartzTriggerEndPointWebExtension(quartzTriggerEndPoint); + } +} diff --git a/src/main/java/org/sathyabodh/actuator/autoconfigure/quartz/QurtzJobEndPointAutoConfiguration.java b/src/main/java/org/sathyabodh/actuator/autoconfigure/quartz/QurtzJobEndPointAutoConfiguration.java new file mode 100644 index 0000000..be8f24d --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/autoconfigure/quartz/QurtzJobEndPointAutoConfiguration.java @@ -0,0 +1,34 @@ +package org.sathyabodh.actuator.autoconfigure.quartz; + +import org.quartz.Scheduler; +import org.quartz.SchedulerFactory; +import org.sathyabodh.actuator.quartz.QuartzJobEndPoint; +import org.sathyabodh.actuator.quartz.QuartzJobEndPointWebExtension; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnClass({ Scheduler.class, SchedulerFactory.class }) +@AutoConfigureAfter(QuartzAutoConfiguration.class) +public class QurtzJobEndPointAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + public QuartzJobEndPoint quartzJobEndPoint(Scheduler scheduler) { + return new QuartzJobEndPoint(scheduler); + } + + @Bean + @ConditionalOnBean(QuartzJobEndPoint.class) + public QuartzJobEndPointWebExtension quartzJobEndPointWebExtension(QuartzJobEndPoint quartzJobEndPoint) { + return new QuartzJobEndPointWebExtension(quartzJobEndPoint); + } + +} diff --git a/src/main/java/org/sathyabodh/actuator/cache/CachesEndPoint.java b/src/main/java/org/sathyabodh/actuator/cache/CachesEndPoint.java new file mode 100644 index 0000000..707e71e --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/cache/CachesEndPoint.java @@ -0,0 +1,77 @@ +package org.sathyabodh.actuator.cache; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.sathyabodh.actuator.cache.exception.NonUniqueCacheException; +import org.sathyabodh.actuator.cache.model.CacheDetailModel; +import org.sathyabodh.actuator.cache.model.CacheEntryModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.lang.Nullable; + +@Endpoint(id="caches") +public class CachesEndPoint { + + private static Logger log = LoggerFactory.getLogger(CachesEndPoint.class); + private Map cacheManagers ; + + public CachesEndPoint(Map cacheManagers) { + this.cacheManagers = cacheManagers; + } + + @ReadOperation + public CacheDetailModel caches(){ + final CacheDetailModel detailModel = new CacheDetailModel(); + cacheManagers.entrySet().forEach(entry->detailModel.get(entry.getKey()).add(entry.getValue())); + return detailModel; + } + + @ReadOperation + public CacheEntryModel cache(@Selector String name, @Nullable String cacheManager){ + return findUniqueCache(name, cacheManager); + } + + @DeleteOperation + public boolean evictCaches(@Selector String name, @Nullable String cacheManager){ + CacheEntryModel cache = findUniqueCache(name, cacheManager); + return cache == null ? false:clearCache(cache); + } + + private boolean clearCache(CacheEntryModel cacheEntryModel){ + CacheManager cacheManager = cacheManagers.get(cacheEntryModel.getCacheManager()); + if(cacheManager == null){ + return false; + } + Cache cache = cacheManager.getCache(cacheEntryModel.getName()); + if(cache == null){ + return false; + } + + log.info("Clearning cache:[{}]", cache.getName()); + cache.clear(); + log.info("Cleared cache:[{}]", cache.getName()); + return true; + } + + private CacheEntryModel findUniqueCache(String name, String cacheManager){ + List caches = cacheManagers.entrySet().stream().filter(it-> cacheManager == null || cacheManager.equals(it.getKey())) + .map(entry->entry.getValue().getCache(name) == null ? null : new CacheEntryModel(entry.getKey(), entry.getValue().getCache(name))) + .filter(entry->entry != null).collect(Collectors.toList()); + if(caches.size() > 1){ + throw new NonUniqueCacheException( + String.format("Non unique cache name:%s. " + + "Please use cacheManager name to uniquely identify", name)); + } + return caches.isEmpty() ? null : caches.get(0); + } + + +} diff --git a/src/main/java/org/sathyabodh/actuator/cache/CachesEndPointWebExtension.java b/src/main/java/org/sathyabodh/actuator/cache/CachesEndPointWebExtension.java new file mode 100644 index 0000000..eda0f1d --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/cache/CachesEndPointWebExtension.java @@ -0,0 +1,51 @@ +package org.sathyabodh.actuator.cache; + +import org.sathyabodh.actuator.cache.exception.NonUniqueCacheException; +import org.sathyabodh.actuator.cache.model.CacheEntryModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; +import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; +import org.springframework.lang.Nullable; + + +@EndpointWebExtension(endpoint = CachesEndPoint.class) +public class CachesEndPointWebExtension { + private static Logger log = LoggerFactory.getLogger(CachesEndPoint.class); + + private CachesEndPoint cachesEndPoint; + + public CachesEndPointWebExtension(CachesEndPoint cachesEndPoint) { + this.cachesEndPoint = cachesEndPoint; + } + + @ReadOperation + public WebEndpointResponse cache(@Selector String name, @Nullable String cacheManager) { + CacheEntryModel model = null; + int status; + try { + model = cachesEndPoint.cache(name, cacheManager); + status = null == model ? WebEndpointResponse.STATUS_NOT_FOUND : WebEndpointResponse.STATUS_OK; + } catch (NonUniqueCacheException e) { + log.error("Error in getting cache detials", e); + status = WebEndpointResponse.STATUS_BAD_REQUEST; + } + return new WebEndpointResponse<>(model, status); + } + + @DeleteOperation + public WebEndpointResponse delete(@Selector String name, @Nullable String cacheManager) { + int status; + try { + boolean isSuccess = cachesEndPoint.evictCaches(name, cacheManager); + status = isSuccess ? WebEndpointResponse.STATUS_OK : WebEndpointResponse.STATUS_NOT_FOUND; + } catch (NonUniqueCacheException e) { + log.error("Error in clearing cache detials", e); + status = WebEndpointResponse.STATUS_BAD_REQUEST; + } + return new WebEndpointResponse<>(status); + } +} diff --git a/src/main/java/org/sathyabodh/actuator/cache/exception/NonUniqueCacheException.java b/src/main/java/org/sathyabodh/actuator/cache/exception/NonUniqueCacheException.java new file mode 100644 index 0000000..58115cd --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/cache/exception/NonUniqueCacheException.java @@ -0,0 +1,13 @@ +package org.sathyabodh.actuator.cache.exception; + +public class NonUniqueCacheException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 1L; + public NonUniqueCacheException(String message){ + super(message); + } + +} diff --git a/src/main/java/org/sathyabodh/actuator/cache/model/CacheDetailModel.java b/src/main/java/org/sathyabodh/actuator/cache/model/CacheDetailModel.java new file mode 100644 index 0000000..2324e7c --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/cache/model/CacheDetailModel.java @@ -0,0 +1,22 @@ +package org.sathyabodh.actuator.cache.model; + +import java.util.HashMap; +import java.util.Map; + +public class CacheDetailModel { + + private Map cacheManagers = new HashMap<>(); + + public CacheManagerModel get(String cacheManager){ + CacheManagerModel model = getCacheManagers().get(cacheManager); + if(model == null){ + model = new CacheManagerModel(); + getCacheManagers().put(cacheManager, model); + } + return model; + } + + public Map getCacheManagers() { + return cacheManagers; + } +} diff --git a/src/main/java/org/sathyabodh/actuator/cache/model/CacheEntryModel.java b/src/main/java/org/sathyabodh/actuator/cache/model/CacheEntryModel.java new file mode 100644 index 0000000..c255dcf --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/cache/model/CacheEntryModel.java @@ -0,0 +1,26 @@ +package org.sathyabodh.actuator.cache.model; + +import org.springframework.cache.Cache; + +public class CacheEntryModel extends CacheModel { + private String name; + private String cacheManager; + + public CacheEntryModel(String cacheManager, Cache cache){ + super(cache.getNativeCache().getClass().getName()); + this.setName(cache.getName()); + this.setCacheManager(cacheManager); + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public String getCacheManager() { + return cacheManager; + } + public void setCacheManager(String cacheManager) { + this.cacheManager = cacheManager; + } +} diff --git a/src/main/java/org/sathyabodh/actuator/cache/model/CacheManagerModel.java b/src/main/java/org/sathyabodh/actuator/cache/model/CacheManagerModel.java new file mode 100644 index 0000000..8990fe2 --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/cache/model/CacheManagerModel.java @@ -0,0 +1,24 @@ +package org.sathyabodh.actuator.cache.model; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +public class CacheManagerModel { + private Map caches = new HashMap<>(); + + public void add(CacheManager cacheManager){ + cacheManager.getCacheNames().forEach(name->addEntry(cacheManager.getCache(name))); + } + + private void addEntry(Cache cache){ + CacheModel model = new CacheModel(cache.getNativeCache().getClass().getName()); + getCaches().put(cache.getName(), model); + } + + public Map getCaches() { + return caches; + } +} diff --git a/src/main/java/org/sathyabodh/actuator/cache/model/CacheModel.java b/src/main/java/org/sathyabodh/actuator/cache/model/CacheModel.java new file mode 100644 index 0000000..c7f3063 --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/cache/model/CacheModel.java @@ -0,0 +1,16 @@ +package org.sathyabodh.actuator.cache.model; + +public class CacheModel { + private String target; + + public CacheModel(String target){ + this.target = target; + } + public String getTarget() { + return target; + } + + public void setTarget(String target) { + this.target = target; + } +} diff --git a/src/main/java/org/sathyabodh/actuator/quartz/QuartzJobEndPoint.java b/src/main/java/org/sathyabodh/actuator/quartz/QuartzJobEndPoint.java new file mode 100644 index 0000000..f8c288e --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/quartz/QuartzJobEndPoint.java @@ -0,0 +1,132 @@ +package org.sathyabodh.actuator.quartz; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.impl.matchers.GroupMatcher; +import org.sathyabodh.actuator.quartz.exception.UnsupportStateChangeException; +import org.sathyabodh.actuator.quartz.model.GroupModel; +import org.sathyabodh.actuator.quartz.model.JobDetailModel; +import org.sathyabodh.actuator.quartz.model.JobModel; +import org.sathyabodh.actuator.quartz.service.TriggerModelBuilder; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +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.lang.Nullable; + +@Endpoint(id = "quartz-jobs") +public class QuartzJobEndPoint { + + private Scheduler scheduler; + + private TriggerModelBuilder triggerModelBuilder = new TriggerModelBuilder(); + + public QuartzJobEndPoint(Scheduler scheduler){ + this.scheduler = scheduler; + } + + @ReadOperation + public GroupModel listJobs(@Nullable String group, @Nullable String name) throws SchedulerException { + try { + if (name != null && group != null) { + JobModel model = createJobModel(new JobKey(name, group)); + if (model == null) { + return null; + } + GroupModel jobGroupModel = new GroupModel<>(); + jobGroupModel.add(group, model); + return jobGroupModel; + } + GroupMatcher jobGroupMatcher = group == null ? + GroupMatcher.anyJobGroup():GroupMatcher.jobGroupEquals(group); + + Set jobKeys = scheduler.getJobKeys(jobGroupMatcher); + if(name != null){ + jobKeys = jobKeys.stream().filter(key->name.equals(key.getName())).collect(Collectors.toSet()); + } + if (jobKeys == null || jobKeys.isEmpty()) { + return null; + } + GroupModel jobGroupModel = new GroupModel<>(); + jobKeys.forEach(key->{JobModel model = createJobModel(key); + jobGroupModel.add(key.getGroup(), model); + }); + return jobGroupModel; + } catch (SchedulerException e) { + throw e; + } + } + + @ReadOperation + public JobDetailModel getJobDetail(@Selector String group, @Selector String name) throws SchedulerException{ + JobDetail jobDetail = scheduler.getJobDetail(new JobKey(name, group)); + JobDetailModel model = new JobDetailModel(); + copyJobDetailModel(jobDetail, model); + model.setGroup(jobDetail.getKey().getGroup()); + model.setTriggers(triggerModelBuilder.buildTriggerDetailModel(scheduler, jobDetail.getKey())); + return model; + } + + private JobModel createJobModel(JobKey key){ + try { + JobDetail jobDetail = scheduler.getJobDetail(key); + if (jobDetail == null) { + return null; + } + JobModel model = new JobModel(); + copyJobDetailModel(jobDetail, model); + return model; + + } catch (SchedulerException e) { + throw new RuntimeException(e); + } + + } + + private void copyJobDetailModel(JobDetail jobDetail,JobModel model) { + model.setName(jobDetail.getKey().getName()); + model.setDurable(jobDetail.isDurable()); + model.setConcurrentDisallowed(jobDetail.isConcurrentExectionDisallowed()); + model.setJobClass(jobDetail.getJobClass().getName()); + } + + @WriteOperation + public boolean modifyJobStatus(@Selector String group, @Selector String name, @Selector String state) + throws SchedulerException { + JobKey jobKey = new JobKey(name, group); + JobDetail detail = scheduler.getJobDetail(jobKey); + if (detail == null) + return false; + else if (QuartzState.PAUSE.equals(state)) { + scheduler.pauseJob(jobKey); + } else if (QuartzState.RESUME.equals(state)) { + scheduler.resumeJob(jobKey); + } else { + throw new UnsupportStateChangeException(String.format("unsupported state change. state:[%s]", state)); + } + return true; + } + + @WriteOperation + public boolean modifyJobsStatus(@Selector String group, @Selector String state) throws SchedulerException{ + GroupMatcher jobGroupMatcher = GroupMatcher.jobGroupEquals(group); + Set jobKeys = scheduler.getJobKeys(jobGroupMatcher); + if (jobKeys == null || jobKeys.isEmpty()) { + return false; + } + else if (QuartzState.PAUSE.equals(state)) { + scheduler.pauseJobs(jobGroupMatcher); + } else if (QuartzState.RESUME.equals(state)) { + scheduler.resumeJobs(jobGroupMatcher); + } else { + throw new UnsupportStateChangeException(String.format("unsupported state change. state:[%s]", state)); + } + return true; + } + +} diff --git a/src/main/java/org/sathyabodh/actuator/quartz/QuartzJobEndPointWebExtension.java b/src/main/java/org/sathyabodh/actuator/quartz/QuartzJobEndPointWebExtension.java new file mode 100644 index 0000000..e3ad360 --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/quartz/QuartzJobEndPointWebExtension.java @@ -0,0 +1,39 @@ +package org.sathyabodh.actuator.quartz; + +import org.quartz.SchedulerException; +import org.sathyabodh.actuator.quartz.exception.UnsupportStateChangeException; +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; + +@EndpointWebExtension(endpoint=QuartzJobEndPoint.class) +public class QuartzJobEndPointWebExtension { + + private QuartzJobEndPoint qurtzJobEndPoint ; + + public QuartzJobEndPointWebExtension(QuartzJobEndPoint qurtzJobEndPoint){ + this.qurtzJobEndPoint = qurtzJobEndPoint; + } + + @WriteOperation + public WebEndpointResponse modifyJobStatus(@Selector String group, @Selector String name, @Selector String state) throws SchedulerException { + try{ + boolean isSucess = qurtzJobEndPoint.modifyJobStatus(group, name, state); + int status = isSucess ? WebEndpointResponse.STATUS_OK : WebEndpointResponse.STATUS_NOT_FOUND; + return new WebEndpointResponse<>(status); + }catch(UnsupportStateChangeException e){ + return new WebEndpointResponse<>(WebEndpointResponse.STATUS_BAD_REQUEST); + } + } + @WriteOperation + public WebEndpointResponse modifyJobsStatus(@Selector String group,@Selector String state) throws SchedulerException { + try{ + boolean isSucess = qurtzJobEndPoint.modifyJobsStatus(group, state); + int status = isSucess ? WebEndpointResponse.STATUS_OK : WebEndpointResponse.STATUS_NOT_FOUND; + return new WebEndpointResponse<>(status); + }catch(UnsupportStateChangeException e){ + return new WebEndpointResponse<>(WebEndpointResponse.STATUS_BAD_REQUEST); + } + } +} diff --git a/src/main/java/org/sathyabodh/actuator/quartz/QuartzState.java b/src/main/java/org/sathyabodh/actuator/quartz/QuartzState.java new file mode 100644 index 0000000..943f000 --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/quartz/QuartzState.java @@ -0,0 +1,6 @@ +package org.sathyabodh.actuator.quartz; + +public interface QuartzState { + String PAUSE="pause"; + String RESUME="resume"; +} diff --git a/src/main/java/org/sathyabodh/actuator/quartz/QuartzTriggerEndPoint.java b/src/main/java/org/sathyabodh/actuator/quartz/QuartzTriggerEndPoint.java new file mode 100644 index 0000000..1aca309 --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/quartz/QuartzTriggerEndPoint.java @@ -0,0 +1,101 @@ +package org.sathyabodh.actuator.quartz; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.Trigger; +import org.quartz.TriggerKey; +import org.quartz.impl.matchers.GroupMatcher; +import org.sathyabodh.actuator.quartz.exception.UnsupportStateChangeException; +import org.sathyabodh.actuator.quartz.model.GroupModel; +import org.sathyabodh.actuator.quartz.model.TriggerDetailModel; +import org.sathyabodh.actuator.quartz.service.TriggerModelBuilder; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +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.lang.Nullable; + +@Endpoint(id = "quartz-triggers") +public class QuartzTriggerEndPoint { + private Scheduler scheduler; + private TriggerModelBuilder triggerModelBuilder = new TriggerModelBuilder(); + + public QuartzTriggerEndPoint(Scheduler scheduler){ + this.scheduler = scheduler; + } + + @ReadOperation + public GroupModel listTriggers(@Nullable String group, @Nullable String name) throws SchedulerException { + try { + if (name != null && group != null) { + TriggerDetailModel model = triggerModelBuilder.buildTriggerDetailModel(scheduler, new TriggerKey(name, group)); + if (model == null) { + return null; + } + GroupModel groupModel = new GroupModel<>(); + groupModel.add(group, model); + return groupModel; + } + GroupMatcher triggerGroupMatcher = group == null ? + GroupMatcher.anyTriggerGroup():GroupMatcher.triggerGroupEquals(group); + Set triggerKeys = scheduler.getTriggerKeys(triggerGroupMatcher); + if(name != null){ + triggerKeys = triggerKeys.stream().filter(key->name.equals(key.getName())).collect(Collectors.toSet()); + } + if (triggerKeys == null || triggerKeys.isEmpty()) { + return null; + } + GroupModel groupModel = new GroupModel<>(); + triggerKeys.forEach(key->addTriggerDetailModel(groupModel, key)); + return groupModel; + } catch (SchedulerException e) { + throw e; + } + } + + private void addTriggerDetailModel(GroupModel groupModel, TriggerKey key){ + TriggerDetailModel model; + try { + model = triggerModelBuilder.buildTriggerDetailModel(scheduler, key); + groupModel.add(key.getGroup(), model); + } catch (SchedulerException e) { + throw new RuntimeException(e); + } + } + + @WriteOperation + public boolean modifyTriggerStatus(@Selector String group, @Selector String name, @Selector String state) + throws SchedulerException { + TriggerKey triggerKey = new TriggerKey(name, group); + Trigger trigger = scheduler.getTrigger(triggerKey); + if (trigger == null) + return false; + else if (QuartzState.PAUSE.equals(state)) { + scheduler.pauseTrigger(triggerKey); + } else if (QuartzState.RESUME.equals(state)) { + scheduler.resumeTrigger(triggerKey); + } else { + throw new UnsupportStateChangeException(String.format("unsupported state change. state:[%s]", state)); + } + return true; + } + + @WriteOperation + public boolean modifyTriggersStatus(@Selector String group, @Selector String state) throws SchedulerException { + GroupMatcher triggerGroupMatcher = GroupMatcher.triggerGroupEquals(group); + Set triggerKeys = scheduler.getTriggerKeys(triggerGroupMatcher); + if (triggerKeys == null || triggerKeys.isEmpty()) { + return false; + } else if (QuartzState.PAUSE.equals(state)) { + scheduler.pauseTriggers(triggerGroupMatcher); + } else if (QuartzState.RESUME.equals(state)) { + scheduler.resumeTriggers((triggerGroupMatcher)); + } else { + throw new UnsupportStateChangeException(String.format("unsupported state change. state:[%s]", state)); + } + return true; + } +} diff --git a/src/main/java/org/sathyabodh/actuator/quartz/QuartzTriggerEndPointWebExtension.java b/src/main/java/org/sathyabodh/actuator/quartz/QuartzTriggerEndPointWebExtension.java new file mode 100644 index 0000000..f7a2cfa --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/quartz/QuartzTriggerEndPointWebExtension.java @@ -0,0 +1,40 @@ +package org.sathyabodh.actuator.quartz; + +import org.quartz.SchedulerException; +import org.sathyabodh.actuator.quartz.exception.UnsupportStateChangeException; +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; + +@EndpointWebExtension(endpoint=QuartzTriggerEndPoint.class) +public class QuartzTriggerEndPointWebExtension { + + private QuartzTriggerEndPoint qurtzTriggerEndPoint ; + + public QuartzTriggerEndPointWebExtension(QuartzTriggerEndPoint qurtzTriggerEndPoint){ + this.qurtzTriggerEndPoint = qurtzTriggerEndPoint; + } + + @WriteOperation + public WebEndpointResponse modifyTriggerStatus(@Selector String group, @Selector String name, @Selector String state) throws SchedulerException { + try{ + boolean isSucess = qurtzTriggerEndPoint.modifyTriggerStatus(group, name, state); + int status = isSucess ? WebEndpointResponse.STATUS_OK : WebEndpointResponse.STATUS_NOT_FOUND; + return new WebEndpointResponse<>(status); + }catch(UnsupportStateChangeException e){ + return new WebEndpointResponse<>(WebEndpointResponse.STATUS_BAD_REQUEST); + } + } + @WriteOperation + public WebEndpointResponse modifyTriggersStatus(@Selector String group,@Selector String state) throws SchedulerException { + try{ + boolean isSucess = qurtzTriggerEndPoint.modifyTriggersStatus(group, state); + int status = isSucess ? WebEndpointResponse.STATUS_OK : WebEndpointResponse.STATUS_NOT_FOUND; + return new WebEndpointResponse<>(status); + }catch(UnsupportStateChangeException e){ + return new WebEndpointResponse<>(WebEndpointResponse.STATUS_BAD_REQUEST); + } + } + +} diff --git a/src/main/java/org/sathyabodh/actuator/quartz/exception/UnsupportStateChangeException.java b/src/main/java/org/sathyabodh/actuator/quartz/exception/UnsupportStateChangeException.java new file mode 100644 index 0000000..708b70c --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/quartz/exception/UnsupportStateChangeException.java @@ -0,0 +1,14 @@ +package org.sathyabodh.actuator.quartz.exception; + +public class UnsupportStateChangeException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public UnsupportStateChangeException(String message){ + super(message); + } + +} diff --git a/src/main/java/org/sathyabodh/actuator/quartz/model/GroupModel.java b/src/main/java/org/sathyabodh/actuator/quartz/model/GroupModel.java new file mode 100644 index 0000000..41d4fea --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/quartz/model/GroupModel.java @@ -0,0 +1,25 @@ +package org.sathyabodh.actuator.quartz.model; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class GroupModel { + + private Map> groups = new HashMap<>(); + + public void add(String group, T model){ + List models = getGroups().get(group); + if(models == null){ + models = new ArrayList<>(); + getGroups().put(group, models); + } + models.add(model); + } + + public Map> getGroups() { + return groups; + } + +} diff --git a/src/main/java/org/sathyabodh/actuator/quartz/model/JobDetailModel.java b/src/main/java/org/sathyabodh/actuator/quartz/model/JobDetailModel.java new file mode 100644 index 0000000..5979056 --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/quartz/model/JobDetailModel.java @@ -0,0 +1,21 @@ +package org.sathyabodh.actuator.quartz.model; + +import java.util.List; + +public class JobDetailModel extends JobModel{ + private String group; + private List triggers; + + public String getGroup() { + return group; + } + public void setGroup(String group) { + this.group = group; + } + public List getTriggers() { + return triggers; + } + public void setTriggers(List triggers) { + this.triggers = triggers; + } +} diff --git a/src/main/java/org/sathyabodh/actuator/quartz/model/JobModel.java b/src/main/java/org/sathyabodh/actuator/quartz/model/JobModel.java new file mode 100644 index 0000000..28b7d90 --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/quartz/model/JobModel.java @@ -0,0 +1,33 @@ +package org.sathyabodh.actuator.quartz.model; + + +public class JobModel { + private String name; + private boolean isConcurrentDisallowed; + private boolean isDurable; + private String jobClass; + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public boolean isConcurrentDisallowed() { + return isConcurrentDisallowed; + } + public void setConcurrentDisallowed(boolean isConcurrentDisallowed) { + this.isConcurrentDisallowed = isConcurrentDisallowed; + } + public boolean isDurable() { + return isDurable; + } + public void setDurable(boolean isDurable) { + this.isDurable = isDurable; + } + public String getJobClass() { + return jobClass; + } + public void setJobClass(String jobClass) { + this.jobClass = jobClass; + } +} diff --git a/src/main/java/org/sathyabodh/actuator/quartz/model/TriggerDetailModel.java b/src/main/java/org/sathyabodh/actuator/quartz/model/TriggerDetailModel.java new file mode 100644 index 0000000..88ae55e --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/quartz/model/TriggerDetailModel.java @@ -0,0 +1,64 @@ +package org.sathyabodh.actuator.quartz.model; + +import java.util.Date; + +public class TriggerDetailModel{ + private String name; + private Date nextFireTime; + private Date previousFireTime; + private Date startTime; + private Date endTime; + private String group; + private String state; + private String jobKey; + + public void setState(String state) { + this.state = state; + } + public String getState() { + return state; + } + + public String getJobKey() { + return jobKey; + } + public void setJobKey(String jobKey) { + this.jobKey = jobKey; + } + public String getGroup() { + return group; + } + public void setGroup(String group) { + this.group = group; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public Date getNextFireTime() { + return nextFireTime; + } + public void setNextFireTime(Date nextFireTime) { + this.nextFireTime = nextFireTime; + } + public Date getPreviousFireTime() { + return previousFireTime; + } + public void setPreviousFireTime(Date previousFireTime) { + this.previousFireTime = previousFireTime; + } + public Date getStartTime() { + return startTime; + } + public void setStartTime(Date startTime) { + this.startTime = startTime; + } + public Date getEndTime() { + return endTime; + } + public void setEndTime(Date endTime) { + this.endTime = endTime; + } +} diff --git a/src/main/java/org/sathyabodh/actuator/quartz/model/TriggerModel.java b/src/main/java/org/sathyabodh/actuator/quartz/model/TriggerModel.java new file mode 100644 index 0000000..7abe96a --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/quartz/model/TriggerModel.java @@ -0,0 +1,56 @@ +package org.sathyabodh.actuator.quartz.model; + +import java.util.Date; + +public class TriggerModel { + private String group; + private String name; + private String state; + private Date nextFireTime; + private Date previousFireTime; + private Date startTime; + private Date endTime; + + public String getGroup() { + return group; + } + public void setGroup(String group) { + this.group = group; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public String getState() { + return state; + } + public void setState(String state) { + this.state = state; + } + public Date getNextFireTime() { + return nextFireTime; + } + public void setNextFireTime(Date nextFireTime) { + this.nextFireTime = nextFireTime; + } + public Date getPreviousFireTime() { + return previousFireTime; + } + public void setPreviousFireTime(Date previousFireTime) { + this.previousFireTime = previousFireTime; + } + public Date getStartTime() { + return startTime; + } + public void setStartTime(Date startTime) { + this.startTime = startTime; + } + public Date getEndTime() { + return endTime; + } + public void setEndTime(Date endTime) { + this.endTime = endTime; + } +} diff --git a/src/main/java/org/sathyabodh/actuator/quartz/service/TriggerModelBuilder.java b/src/main/java/org/sathyabodh/actuator/quartz/service/TriggerModelBuilder.java new file mode 100644 index 0000000..4cc9f4b --- /dev/null +++ b/src/main/java/org/sathyabodh/actuator/quartz/service/TriggerModelBuilder.java @@ -0,0 +1,45 @@ +package org.sathyabodh.actuator.quartz.service; + +import java.util.List; +import java.util.stream.Collectors; + +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.Trigger; +import org.quartz.Trigger.TriggerState; +import org.quartz.TriggerKey; +import org.sathyabodh.actuator.quartz.model.TriggerDetailModel; + +public class TriggerModelBuilder { + + public List buildTriggerDetailModel(Scheduler scheduler, JobKey jobKey) throws SchedulerException{ + List triggers = scheduler.getTriggersOfJob(jobKey); + return triggers.stream().map(t->buildTriggerDetailModel(scheduler, t)).collect(Collectors.toList()); + } + + public TriggerDetailModel buildTriggerDetailModel(Scheduler scheduler, TriggerKey triggerKey) throws SchedulerException{ + Trigger trigger = scheduler.getTrigger(triggerKey); + return buildTriggerDetailModel(scheduler, trigger); + } + + private TriggerDetailModel buildTriggerDetailModel(Scheduler scheduler, Trigger trigger){ + TriggerDetailModel model = new TriggerDetailModel(); + model.setName(trigger.getKey().getName()); + model.setNextFireTime(trigger.getNextFireTime()); + model.setPreviousFireTime(trigger.getPreviousFireTime()); + model.setStartTime(trigger.getStartTime()); + model.setEndTime(trigger.getEndTime()); + model.setGroup(trigger.getKey().getGroup()); + model.setJobKey(trigger.getJobKey().toString()); + TriggerState triggerState; + try { + triggerState = scheduler.getTriggerState(trigger.getKey()); + model.setState(triggerState.toString()); + } catch (SchedulerException e) { + model.setState("Could not set due to error"); + } + return model; + } + +} diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..74a6599 --- /dev/null +++ b/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.sathyabodh.actuator.autoconfigure.cache.CachesEndPointAutoConfiguration,\ +org.sathyabodh.actuator.autoconfigure.quartz.QurtzJobEndPointAutoConfiguration,\ +org.sathyabodh.actuator.autoconfigure.quartz.QuartzTriggerEndPointAutoConfiguration \ No newline at end of file diff --git a/src/test/java/org/sathyabodh/actuator/web/documentation/AbstractQuartzEndPointDocumentationTests.java b/src/test/java/org/sathyabodh/actuator/web/documentation/AbstractQuartzEndPointDocumentationTests.java new file mode 100644 index 0000000..bce50fc --- /dev/null +++ b/src/test/java/org/sathyabodh/actuator/web/documentation/AbstractQuartzEndPointDocumentationTests.java @@ -0,0 +1,56 @@ +package org.sathyabodh.actuator.web.documentation; + +import org.junit.Before; +import org.junit.Rule; +import org.quartz.Scheduler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.reactive.WebFluxEndpointManagementContextConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Configuration; +import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +public class AbstractQuartzEndPointDocumentationTests { + @Rule + public JUnitRestDocumentation documentation = new JUnitRestDocumentation("target/generated-snippets"); + @Autowired + protected WebApplicationContext context; + + protected MockMvc mockMvc; + + @MockBean + protected Scheduler scheduler; + + @Before + public void setUp() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(MockMvcRestDocumentation.documentationConfiguration(this.documentation)).build(); + } + + @Configuration + @ImportAutoConfiguration({ JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, WebMvcAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, + WebMvcEndpointManagementContextConfiguration.class, + WebFluxEndpointManagementContextConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, WebFluxAutoConfiguration.class, + HttpHandlerAutoConfiguration.class }) +static class BaseDocumentationConfiguration { +} + +} diff --git a/src/test/java/org/sathyabodh/actuator/web/documentation/QuartzJobEndPointDocumentationTests.java b/src/test/java/org/sathyabodh/actuator/web/documentation/QuartzJobEndPointDocumentationTests.java new file mode 100644 index 0000000..812d719 --- /dev/null +++ b/src/test/java/org/sathyabodh/actuator/web/documentation/QuartzJobEndPointDocumentationTests.java @@ -0,0 +1,195 @@ +package org.sathyabodh.actuator.web.documentation; + +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.quartz.CronScheduleBuilder; +import org.quartz.DisallowConcurrentExecution; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.JobKey; +import org.quartz.ScheduleBuilder; +import org.quartz.Scheduler; +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.sathyabodh.actuator.quartz.QuartzJobEndPoint; +import org.sathyabodh.actuator.quartz.QuartzJobEndPointWebExtension; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +@RunWith(SpringRunner.class) +@SpringBootTest +@TestPropertySource(properties={ + "management.endpoints.web.exposure.include=*", + "management.endpoints.web.basePath=/actuator" +}) +public class QuartzJobEndPointDocumentationTests extends AbstractQuartzEndPointDocumentationTests { + + @Test + public void testQuartzJobPause() throws Exception { + JobKey jobKey = new JobKey("myjob", "myjobgroup"); + JobDetail jobDetail = JobBuilder.newJob(Job.class).withIdentity(jobKey).build(); + Mockito.when(scheduler.getJobDetail(jobKey)).thenReturn(jobDetail); + Mockito.doNothing().when(scheduler).pauseJob(jobKey); + this.mockMvc.perform(MockMvcRequestBuilders.post("/actuator/quartz-jobs/myjobgroup/myjob/pause")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("quartz-jobs/named/job/pause")); + } + + @Test + public void testQuartzJobResume() throws Exception { + JobKey jobKey = new JobKey("myjob", "myjobgroup"); + JobDetail jobDetail = JobBuilder.newJob(Job.class).withIdentity(jobKey).build(); + Mockito.when(scheduler.getJobDetail(jobKey)).thenReturn(jobDetail); + Mockito.doNothing().when(scheduler).pauseJob(jobKey); + this.mockMvc.perform(MockMvcRequestBuilders.post("/actuator/quartz-jobs/myjobgroup/myjob/resume")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("quartz-jobs/named/job/resume")); + } + + @Test + public void testQuartzJobGroupPause() throws Exception { + JobKey jobKey = new JobKey("myjob", "myjobgroup"); + Set jobKeys = new HashSet<>(); + jobKeys.add(jobKey); + GroupMatcher jobGroupMatcher = GroupMatcher.jobGroupEquals("myjobgroup"); + Mockito.when(scheduler.getJobKeys(jobGroupMatcher)).thenReturn(jobKeys); + Mockito.doNothing().when(scheduler).pauseJobs(jobGroupMatcher); + this.mockMvc.perform(MockMvcRequestBuilders.post("/actuator/quartz-jobs/myjobgroup/pause")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("quartz-jobs/named/jobgroup/pause")); + } + + @Test + public void testQuartzJobGroupResume() throws Exception { + JobKey jobKey = new JobKey("myjob", "myjobgroup"); + Set jobKeys = new HashSet<>(); + jobKeys.add(jobKey); + GroupMatcher jobGroupMatcher = GroupMatcher.jobGroupEquals("myjobgroup"); + Mockito.when(scheduler.getJobKeys(jobGroupMatcher)).thenReturn(jobKeys); + Mockito.doNothing().when(scheduler).pauseJobs(jobGroupMatcher); + this.mockMvc.perform(MockMvcRequestBuilders.post("/actuator/quartz-jobs/myjobgroup/resume")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("quartz-jobs/named/jobgroup/resume")); + } + + @Test + public void testQuartzJobList() throws Exception { + int i = 1; + Set jobKeys = new HashSet<>(); + List jobDetails = new ArrayList<>(); + while (i <= 3) { + JobKey jobKey = new JobKey("myjob_" + i, "myjobgroup"); + JobDetail jobDetail = JobBuilder.newJob(MockNonConcurrentJob.class).withIdentity(jobKey).storeDurably() + .build(); + ++i; + jobKeys.add(jobKey); + jobDetails.add(jobDetail); + } + + GroupMatcher jobGroupMatcher = GroupMatcher.jobGroupEquals("myjobgroup"); + Mockito.when(scheduler.getJobKeys(jobGroupMatcher)).thenReturn(jobKeys); + + Mockito.when(scheduler.getJobDetail(Mockito.any(JobKey.class))).thenReturn(jobDetails.get(0)); + this.mockMvc.perform(MockMvcRequestBuilders.get("/actuator/quartz-jobs?group=myjobgroup&name=myjob_1")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("quartz-jobs/list", + requestParameters(parameterWithName("group").description("Name of the job group").optional(), + parameterWithName("name").description("Name of the job").optional()), + responseFields(fieldWithPath("groups").description("Job group keyed by group name"), + fieldWithPath("groups.*.[].name").description("Name of the job"), + fieldWithPath("groups.*.[].concurrentDisallowed") + .description("Indicates concurrent execution of job is allowed"), + fieldWithPath("groups.*.[].durable").description("Indicates job is durable"), + fieldWithPath("groups.*.[].jobClass").description("Class name of the job")))); + } + + @Test + public void testGetJobDetails() throws Exception { + JobKey jobKey = new JobKey("myjob", "myjobgroup"); + JobDetail jobDetail = JobBuilder.newJob(MockNonConcurrentJob.class).withIdentity(jobKey).storeDurably().build(); + + Trigger trigger1 = TriggerBuilder.newTrigger().forJob(jobDetail).withIdentity("trigger_1", "trigger_group1") + .startAt(new Date()).build(); + + ScheduleBuilder builder = CronScheduleBuilder.cronSchedule("0 0 23 * * ?") + .withMisfireHandlingInstructionFireAndProceed(); + Trigger trigger2 = TriggerBuilder.newTrigger().forJob(jobDetail).withIdentity("trigger_2", "trigger_group1") + .withSchedule(builder).build(); + + List triggers = new ArrayList(); + triggers.add(trigger1); + triggers.add(trigger2); + + Mockito.when(scheduler.getJobDetail(Mockito.any(JobKey.class))).thenReturn(jobDetail); + Mockito.when(scheduler.getTriggersOfJob(Mockito.any(JobKey.class))).thenReturn(triggers); + Mockito.when(scheduler.getTriggerState(Mockito.any(TriggerKey.class))).thenReturn(TriggerState.NORMAL); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/actuator/quartz-jobs/myjobgroup/myjob")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("quartz-jobs/named/jobdetails", + responseFields(fieldWithPath("name").description("Name of the job"), + fieldWithPath("group").description("Group name of the job"), + fieldWithPath("concurrentDisallowed") + .description("Indicates concurrent execution of job is allowed"), + fieldWithPath("durable").description("Indicates job is durable"), + fieldWithPath("jobClass").description("Class name of the job"), + fieldWithPath("triggers").description("Triggers associated with job"), + fieldWithPath("triggers.[].name").description("Name of the trigger"), + fieldWithPath("triggers.[].nextFireTime").description("Next fire time of the trigger"), + fieldWithPath("triggers.[].previousFireTime") + .description("Previous fire time of the trigger"), + fieldWithPath("triggers.[].startTime").description("Start time of the trigger"), + fieldWithPath("triggers.[].endTime").description("End time of the trigger"), + fieldWithPath("triggers.[].group").description("Trigger's group"), + fieldWithPath("triggers.[].state").description("State of trigger ex:PAUSED,NORMAL"), + fieldWithPath("triggers.[].jobKey").description("Associated Job key of the trigger")))); + } + + @DisallowConcurrentExecution + class MockNonConcurrentJob implements Job { + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + } + } + + @Configuration + @Import(BaseDocumentationConfiguration.class) + static class QuartzJobEndPointConfiguration { + @Bean + public QuartzJobEndPoint quartzJobEndPoint(Scheduler scheduler) { + return new QuartzJobEndPoint(scheduler); + } + + @Bean + public QuartzJobEndPointWebExtension quartzJobEndPointWebExtension(QuartzJobEndPoint quartzJobEndPoint) { + return new QuartzJobEndPointWebExtension(quartzJobEndPoint); + } + + } +} diff --git a/src/test/java/org/sathyabodh/actuator/web/documentation/QuartzTriggerEndPointDocumentationTests.java b/src/test/java/org/sathyabodh/actuator/web/documentation/QuartzTriggerEndPointDocumentationTests.java new file mode 100644 index 0000000..42aaba5 --- /dev/null +++ b/src/test/java/org/sathyabodh/actuator/web/documentation/QuartzTriggerEndPointDocumentationTests.java @@ -0,0 +1,108 @@ +package org.sathyabodh.actuator.web.documentation; + +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; + +import java.util.Date; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.Trigger; +import org.quartz.Trigger.TriggerState; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import org.sathyabodh.actuator.quartz.QuartzTriggerEndPoint; +import org.sathyabodh.actuator.quartz.QuartzTriggerEndPointWebExtension; +import org.sathyabodh.actuator.web.documentation.QuartzJobEndPointDocumentationTests.MockNonConcurrentJob; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +@RunWith(SpringRunner.class) +@SpringBootTest +@TestPropertySource(properties={ + "management.endpoints.web.exposure.include=*", + "management.endpoints.web.basePath=/actuator" +}) +public class QuartzTriggerEndPointDocumentationTests extends AbstractQuartzEndPointDocumentationTests { + + @Test + public void testTriggerListing() throws Exception { + + JobDetail jobDetail = JobBuilder.newJob(MockNonConcurrentJob.class) + .withIdentity(new JobKey("myjob", "myjobGroup")).storeDurably().build(); + Trigger trigger1 = TriggerBuilder.newTrigger().forJob(jobDetail).withIdentity("trigger_1", "trigger_group1") + .startAt(new Date()).build(); + Mockito.when(scheduler.getTrigger(Mockito.any(TriggerKey.class))).thenReturn(trigger1); + Mockito.when(scheduler.getTriggerState(Mockito.any(TriggerKey.class))).thenReturn(TriggerState.NORMAL); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/actuator/quartz-triggers?group=myjobgroup&name=myjob_")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("quartz-triggers/list", + requestParameters(parameterWithName("group").description("Name of the trigger").optional(), + parameterWithName("name").description("Name of the trigger").optional()), + responseFields(fieldWithPath("groups").description("Trigger group keyed by group name"), + fieldWithPath("groups.*.[].name").description("Name of the trigger"), + fieldWithPath("groups.*.[].nextFireTime").description("Next fire time of the trigger"), + fieldWithPath("groups.*.[].previousFireTime") + .description("Previous fire time of the trigger"), + fieldWithPath("groups.*.[].startTime").description("Start time of the trigger"), + fieldWithPath("groups.*.[].endTime").description("End time of the trigger"), + fieldWithPath("groups.*.[].group").description("Trigger's group"), + fieldWithPath("groups.*.[].state").description("State of trigger ex:PAUSED,NORMAL"), + fieldWithPath("groups.*.[].jobKey").description("Associated Job key of the trigger")))); + } + + @Test + public void testTriggerPause() throws Exception { + JobDetail jobDetail = JobBuilder.newJob(MockNonConcurrentJob.class) + .withIdentity(new JobKey("myjob", "myjobGroup")).storeDurably().build(); + Trigger trigger1 = TriggerBuilder.newTrigger().forJob(jobDetail).withIdentity("trigger_1", "trigger_group1") + .startAt(new Date()).build(); + Mockito.when(scheduler.getTrigger(Mockito.any(TriggerKey.class))).thenReturn(trigger1); + this.mockMvc.perform(MockMvcRequestBuilders.post("/actuator/quartz-triggers/mytriggergroup/mytrigger/pause")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("quartz-triggers/named/trigger/pause")); + + } + + @Test + public void testTriggerResume() throws Exception { + JobDetail jobDetail = JobBuilder.newJob(MockNonConcurrentJob.class) + .withIdentity(new JobKey("myjob", "myjobGroup")).storeDurably().build(); + Trigger trigger1 = TriggerBuilder.newTrigger().forJob(jobDetail).withIdentity("trigger_1", "trigger_group1") + .startAt(new Date()).build(); + Mockito.when(scheduler.getTrigger(Mockito.any(TriggerKey.class))).thenReturn(trigger1); + this.mockMvc.perform(MockMvcRequestBuilders.post("/actuator/quartz-triggers/mytriggergroup/mytrigger/resume")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcRestDocumentation.document("quartz-triggers/named/trigger/resume")); + } + + @Configuration + @Import(BaseDocumentationConfiguration.class) + static class QuartzTriggerEndPointConfiguration { + @Bean + public QuartzTriggerEndPoint quartzTriggerEndPoint(Scheduler scheduler) { + return new QuartzTriggerEndPoint(scheduler); + } + + @Bean + QuartzTriggerEndPointWebExtension quartzTriggerEndPointWebExtension( + QuartzTriggerEndPoint quartzTriggerEndPoint) { + return new QuartzTriggerEndPointWebExtension(quartzTriggerEndPoint); + } + } +}