001/**
002 * Copyright 2005-2017 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.kew.impl.stuck;
017
018import org.kuali.rice.core.api.config.property.RuntimeConfig;
019import org.kuali.rice.core.api.config.property.RuntimeConfigSet;
020import org.quartz.CronScheduleBuilder;
021import org.quartz.JobBuilder;
022import org.quartz.JobDetail;
023import org.quartz.JobKey;
024import org.quartz.Scheduler;
025import org.quartz.SchedulerException;
026import org.quartz.Trigger;
027import org.quartz.TriggerBuilder;
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030import org.springframework.beans.factory.annotation.Required;
031import org.springframework.context.ApplicationListener;
032import org.springframework.context.event.ContextRefreshedEvent;
033
034/**
035 * @author Eric Westfall
036 */
037public class StuckDocumentScheduler implements ApplicationListener<ContextRefreshedEvent> {
038
039    private static final Logger LOG = LoggerFactory.getLogger(StuckDocumentScheduler.class);
040
041    protected static final JobKey NOTIFICATION_JOB_KEY = JobKey.jobKey("StuckDocuments", "Notification");
042    protected static final JobKey AUTOFIX_COLLECTOR_JOB_KEY = JobKey.jobKey("StuckDocuments", "AutofixCollector");
043
044    private Scheduler scheduler;
045
046    private RuntimeConfig notificationEnabled;
047    private RuntimeConfig notificationCronExpression;
048
049    private RuntimeConfig autofixEnabled;
050    private RuntimeConfig autofixCronExpression;
051    private RuntimeConfig autofixQuietPeriod;
052    private RuntimeConfig autofixMaxAttempts;
053
054    @Override
055    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
056        achieveDesiredState();
057        RuntimeConfigSet configSet = new RuntimeConfigSet(notificationEnabled, notificationCronExpression, autofixEnabled, autofixCronExpression, autofixQuietPeriod, autofixMaxAttempts);
058        configSet.listen(this::configChanged);
059    }
060
061    private void configChanged(RuntimeConfigSet configSet) {
062        LOG.info("StuckDocumentScheduler config was changed, rebuilding job.");
063        achieveDesiredState();
064    }
065
066    private void achieveDesiredState() {
067        try {
068            scheduleNotificationJob();
069            scheduleAutofixCollectorJob();
070        } catch (SchedulerException e) {
071            throw new IllegalStateException("Scheduling failure when attempting to configure Stuck Document jobs", e);
072        }
073    }
074
075    private void scheduleNotificationJob() throws SchedulerException {
076        unscheduleJobIfExists(NOTIFICATION_JOB_KEY);
077        if (notificationEnabled.getValueAsBoolean()) {
078            JobDetail job = JobBuilder.newJob(StuckDocumentNotificationJob.class)
079                    .withIdentity(NOTIFICATION_JOB_KEY).build();
080
081            LOG.info("Stuck Documents Notification job is enabled, scheduling with cron expression " + notificationCronExpression.getValue());
082            CronScheduleBuilder scheduleBuilder =
083                    CronScheduleBuilder.cronSchedule(notificationCronExpression.getValue())
084                            .withMisfireHandlingInstructionDoNothing();
085            Trigger trigger = TriggerBuilder.newTrigger()
086                    .forJob(job)
087                    .startNow()
088                    .withSchedule(scheduleBuilder).build();
089            scheduler().scheduleJob(job, trigger);
090        } else {
091            LOG.info("Stuck Documents Notification job is disabled.");
092        }
093    }
094
095    private void scheduleAutofixCollectorJob() throws SchedulerException {
096        unscheduleJobIfExists(AUTOFIX_COLLECTOR_JOB_KEY);
097        if (autofixEnabled.getValueAsBoolean()) {
098            JobDetail job = JobBuilder.newJob(AutofixCollectorJob.class)
099                    .withIdentity(AUTOFIX_COLLECTOR_JOB_KEY)
100                    .usingJobData(AutofixCollectorJob.AUTOFIX_QUIET_PERIOD_KEY, autofixQuietPeriod.getValueAsInteger())
101                    .usingJobData(AutofixCollectorJob.AUTOFIX_MAX_ATTEMPTS_KEY, autofixMaxAttempts.getValueAsInteger()).build();
102
103            LOG.info("Stuck Documents Autofix job is enabled, scheduling with cron expression " + autofixCronExpression.getValue());
104            CronScheduleBuilder scheduleBuilder =
105                    CronScheduleBuilder.cronSchedule(autofixCronExpression.getValue())
106                            .withMisfireHandlingInstructionDoNothing();
107            Trigger trigger = TriggerBuilder.newTrigger()
108                    .forJob(job)
109                    .startNow()
110                    .withSchedule(scheduleBuilder).build();
111            scheduler().scheduleJob(job, trigger);
112        } else {
113            LOG.info("Stuck Documents Autofix job is disabled.");
114        }
115    }
116
117    private void unscheduleJobIfExists(JobKey jobKey) throws SchedulerException {
118       if (scheduler != null && scheduler().checkExists(jobKey)) {
119            scheduler().deleteJob(jobKey);
120        }
121    }
122
123    private Scheduler scheduler() {
124        if (this.scheduler == null) {
125            throw new IllegalStateException("StuckDocumentScheduler is trying to use the Scheduler but none exists!");
126        }
127        return this.scheduler;
128    }
129
130    /**
131     * Not marked as required because it may be null if running KSB in THIN mode. This is mostly to accomodate the way
132     * that KFS has Rice wired up in their integration tests.
133     */
134    public void setScheduler(Scheduler scheduler) {
135        this.scheduler = scheduler;
136    }
137
138    @Required
139    public void setNotificationEnabled(RuntimeConfig notificationEnabled) {
140        this.notificationEnabled = notificationEnabled;
141    }
142
143    @Required
144    public void setNotificationCronExpression(RuntimeConfig notificationCronExpression) {
145        this.notificationCronExpression = notificationCronExpression;
146    }
147
148    @Required
149    public void setAutofixEnabled(RuntimeConfig autofixEnabled) {
150        this.autofixEnabled = autofixEnabled;
151    }
152
153    @Required
154    public void setAutofixCronExpression(RuntimeConfig autofixCronExpression) {
155        this.autofixCronExpression = autofixCronExpression;
156    }
157
158    @Required
159    public void setAutofixQuietPeriod(RuntimeConfig autofixQuietPeriod) {
160        this.autofixQuietPeriod = autofixQuietPeriod;
161    }
162
163    @Required
164    public void setAutofixMaxAttempts(RuntimeConfig autofixMaxAttempts) {
165        this.autofixMaxAttempts = autofixMaxAttempts;
166    }
167
168}