org.apache.deltaspike.scheduler.impl.AbstractQuartzScheduler Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* http://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.apache.deltaspike.scheduler.impl;
import org.apache.deltaspike.cdise.api.ContextControl;
import org.apache.deltaspike.core.api.config.ConfigResolver;
import org.apache.deltaspike.core.api.provider.BeanProvider;
import org.apache.deltaspike.core.api.provider.DependentProvider;
import org.apache.deltaspike.core.util.ClassDeactivationUtils;
import org.apache.deltaspike.core.util.ClassUtils;
import org.apache.deltaspike.core.util.ExceptionUtils;
import org.apache.deltaspike.core.util.PropertyFileUtils;
import org.apache.deltaspike.core.util.ProxyUtils;
import org.apache.deltaspike.core.util.metadata.AnnotationInstanceProvider;
import org.apache.deltaspike.scheduler.api.Scheduled;
import org.apache.deltaspike.scheduler.spi.Scheduler;
import org.quartz.CronScheduleBuilder;
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.JobListener;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Stack;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
public abstract class AbstractQuartzScheduler implements Scheduler
{
private static final Logger LOG = Logger.getLogger(AbstractQuartzScheduler.class.getName());
private static final Scheduled DEFAULT_SCHEDULED_LITERAL = AnnotationInstanceProvider.of(Scheduled.class);
private static ThreadLocal currentJobListenerContext = new ThreadLocal();
protected org.quartz.Scheduler scheduler;
@Override
public void start()
{
if (this.scheduler != null)
{
throw new UnsupportedOperationException("the scheduler is started already");
}
SchedulerFactory schedulerFactory = null;
try
{
Properties properties = new Properties();
properties.put(StdSchedulerFactory.PROP_SCHED_JOB_FACTORY_CLASS, CdiAwareJobFactory.class.getName());
try
{
ResourceBundle config = loadCustomQuartzConfig();
Enumeration keys = config.getKeys();
String key;
while (keys.hasMoreElements())
{
key = keys.nextElement();
properties.put(key, config.getString(key));
}
}
catch (Exception e1)
{
LOG.info("no custom quartz-config file found. falling back to the default config provided by quartz.");
InputStream inputStream = null;
try
{
inputStream = ClassUtils.getClassLoader(null).getResourceAsStream("org/quartz/quartz.properties");
properties.load(inputStream);
}
catch (Exception e2)
{
LOG.warning("failed to load quartz default-config");
schedulerFactory = new StdSchedulerFactory();
}
finally
{
if (inputStream != null)
{
inputStream.close();
}
}
}
if (schedulerFactory == null)
{
schedulerFactory = new StdSchedulerFactory(properties);
}
}
catch (Exception e)
{
LOG.log(Level.WARNING, "fallback to default scheduler-factory", e);
schedulerFactory = new StdSchedulerFactory();
}
try
{
this.scheduler = schedulerFactory.getScheduler();
if (SchedulerBaseConfig.LifecycleIntegration.START_SCOPES_PER_JOB)
{
this.scheduler.getListenerManager().addJobListener(new InjectionAwareJobListener());
}
if (!this.scheduler.isStarted())
{
this.scheduler.startDelayed(SchedulerBaseConfig.LifecycleIntegration.DELAYED_START_IN_SECONDS);
}
}
catch (SchedulerException e)
{
throw ExceptionUtils.throwAsRuntimeException(e);
}
}
protected ResourceBundle loadCustomQuartzConfig()
{
//don't use quartz.properties as default-value
String configFile = SchedulerBaseConfig.SCHEDULER_CONFIG_FILE;
return PropertyFileUtils.getResourceBundle(configFile);
}
@Override
public void stop()
{
try
{
if (this.scheduler != null && this.scheduler.isStarted())
{
this.scheduler.shutdown(!SchedulerBaseConfig.LifecycleIntegration.FORCE_STOP);
this.scheduler = null;
}
}
catch (SchedulerException e)
{
throw ExceptionUtils.throwAsRuntimeException(e);
}
}
@Override
public void registerNewJob(Class extends T> jobClass)
{
JobKey jobKey = createJobKey(jobClass);
try
{
Scheduled scheduled = jobClass.getAnnotation(Scheduled.class);
String description = scheduled.description();
if ("".equals(scheduled.description()))
{
description = jobClass.getName();
}
JobDetail jobDetail = this.scheduler.getJobDetail(jobKey);
Trigger trigger;
if (jobDetail == null)
{
Class extends Job> jobClassToAdd = createFinalJobClass(jobClass);
jobDetail = JobBuilder.newJob(jobClassToAdd)
.withDescription(description)
.withIdentity(jobKey)
.build();
scheduleNewJob(scheduled, jobKey, jobDetail);
}
else if (scheduled.overrideOnStartup())
{
List extends Trigger> existingTriggers = this.scheduler.getTriggersOfJob(jobKey);
if (existingTriggers == null || existingTriggers.isEmpty())
{
scheduleNewJob(scheduled, jobKey, jobDetail);
return;
}
if (existingTriggers.size() > 1)
{
throw new IllegalStateException("multiple triggers found for " + jobKey + " ('" + jobDetail + "')" +
", but aren't supported by @" + Scheduled.class.getName() + "#overrideOnStartup");
}
trigger = existingTriggers.iterator().next();
if (scheduled.cronExpression().startsWith("{") && scheduled.cronExpression().endsWith("}"))
{
this.scheduler.unscheduleJobs(Arrays.asList(trigger.getKey()));
scheduleNewJob(scheduled, jobKey, jobDetail);
}
else
{
trigger = TriggerBuilder.newTrigger()
.withIdentity(trigger.getKey())
.withSchedule(CronScheduleBuilder.cronSchedule(scheduled.cronExpression()))
.build();
this.scheduler.rescheduleJob(trigger.getKey(), trigger);
}
}
else
{
Logger.getLogger(AbstractQuartzScheduler.class.getName()).info(
jobKey + " exists already and will be ignored.");
}
}
catch (SchedulerException e)
{
throw ExceptionUtils.throwAsRuntimeException(e);
}
}
private void scheduleNewJob(Scheduled scheduled, JobKey jobKey, JobDetail jobDetail) throws SchedulerException
{
String cronExpression = evaluateExpression(scheduled);
this.scheduler.scheduleJob(jobDetail, createTrigger(scheduled, jobKey, cronExpression));
}
private Trigger createTrigger(Scheduled scheduled, JobKey jobKey, String cronExpression) throws SchedulerException
{
UUID triggerKey = UUID.randomUUID();
if (!scheduled.cronExpression().endsWith(cronExpression))
{
createExpressionObserverJob(jobKey, triggerKey, scheduled.cronExpression(), cronExpression);
}
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(jobKey)
.withIdentity(triggerKey.toString())
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
.build();
return trigger;
}
private void createExpressionObserverJob(
JobKey jobKey, UUID triggerKey, String configExpression, String cronExpression) throws SchedulerException
{
if (!ClassDeactivationUtils.isActivated(DynamicExpressionObserverJob.class))
{
return;
}
JobKey observerJobKey =
new JobKey(jobKey.getName() + DynamicExpressionObserverJob.OBSERVER_POSTFIX, jobKey.getGroup());
JobDetail jobDetail = JobBuilder.newJob(DynamicExpressionObserverJob.class)
.usingJobData(DynamicExpressionObserverJob.CONFIG_EXPRESSION_KEY, configExpression)
.usingJobData(DynamicExpressionObserverJob.TRIGGER_ID_KEY, triggerKey.toString())
.usingJobData(DynamicExpressionObserverJob.ACTIVE_CRON_EXPRESSION_KEY, cronExpression)
.withDescription("Config observer for: " + jobKey)
.withIdentity(observerJobKey)
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(observerJobKey)
.withSchedule(CronScheduleBuilder.cronSchedule(
SchedulerBaseConfig.JobCustomization.DYNAMIC_EXPRESSION_OBSERVER_INTERVAL))
.build();
this.scheduler.scheduleJob(jobDetail, trigger);
}
private String evaluateExpression(Scheduled scheduled)
{
String expression = scheduled.cronExpression();
if (expression.startsWith("{") && expression.endsWith("}"))
{
String configKey = expression.substring(1, expression.length() - 1);
expression = ConfigResolver.getProjectStageAwarePropertyValue(configKey, null);
if (expression == null)
{
throw new IllegalStateException("No config-value found for config-key: " + configKey);
}
}
return expression;
}
protected abstract Class extends Job> createFinalJobClass(Class extends T> jobClass);
@Override
public void startJobManually(Class extends T> jobClass)
{
try
{
this.scheduler.triggerJob(createJobKey(jobClass));
}
catch (SchedulerException e)
{
throw ExceptionUtils.throwAsRuntimeException(e);
}
}
@Override
public void interruptJob(Class extends T> jobClass)
{
try
{
this.scheduler.interrupt(createJobKey(jobClass));
}
catch (SchedulerException e)
{
throw ExceptionUtils.throwAsRuntimeException(e);
}
}
@Override
public boolean deleteJob(Class extends T> jobClass)
{
try
{
return this.scheduler.deleteJob(createJobKey(jobClass));
}
catch (SchedulerException e)
{
throw ExceptionUtils.throwAsRuntimeException(e);
}
}
@Override
public void pauseJob(Class extends T> jobClass)
{
try
{
this.scheduler.pauseJob(createJobKey(jobClass));
}
catch (SchedulerException e)
{
throw ExceptionUtils.throwAsRuntimeException(e);
}
}
@Override
public void resumeJob(Class extends T> jobClass)
{
try
{
this.scheduler.resumeJob(createJobKey(jobClass));
}
catch (SchedulerException e)
{
throw ExceptionUtils.throwAsRuntimeException(e);
}
}
@Override
public boolean isExecutingJob(Class extends T> jobClass)
{
try
{
JobKey jobKey = createJobKey(jobClass);
JobDetail jobDetail = this.scheduler.getJobDetail(jobKey);
if (jobDetail == null)
{
return false;
}
for (JobExecutionContext jobExecutionContext : this.scheduler.getCurrentlyExecutingJobs())
{
if (jobKey.equals(jobExecutionContext.getJobDetail().getKey()))
{
return true;
}
}
return false;
}
catch (SchedulerException e)
{
throw ExceptionUtils.throwAsRuntimeException(e);
}
}
private JobKey createJobKey(Class> jobClass)
{
Scheduled scheduled = jobClass.getAnnotation(Scheduled.class);
if (scheduled == null)
{
throw new IllegalStateException("@" + Scheduled.class.getName() + " is missing on " + jobClass.getName());
}
String groupName = scheduled.group().getSimpleName();
String jobName = getJobName(jobClass);
if (!Scheduled.class.getSimpleName().equals(groupName))
{
return new JobKey(jobName, groupName);
}
return new JobKey(jobName);
}
protected String getJobName(Class> jobClass)
{
return jobClass.getSimpleName();
}
private class InjectionAwareJobListener implements JobListener
{
@Override
public String getName()
{
return getClass().getName();
}
@Override
public void jobToBeExecuted(JobExecutionContext jobExecutionContext)
{
Class> jobClass = ProxyUtils.getUnproxiedClass(jobExecutionContext.getJobInstance().getClass());
Scheduled scheduled = jobClass.getAnnotation(Scheduled.class);
//can happen with manually registered job-instances (via #unwrap)
if (scheduled == null && !jobClass.equals(DynamicExpressionObserverJob.class))
{
scheduled = DEFAULT_SCHEDULED_LITERAL;
}
if (scheduled == null)
{
return;
}
JobListenerContext jobListenerContext = new JobListenerContext();
currentJobListenerContext.set(jobListenerContext);
jobListenerContext.startContexts(scheduled);
boolean jobInstanceIsBean;
try
{
jobInstanceIsBean =
Boolean.TRUE.equals(jobExecutionContext.getScheduler().getContext().get(jobClass.getName()));
}
catch (SchedulerException e)
{
jobInstanceIsBean = false;
}
if (!jobInstanceIsBean)
{
BeanProvider.injectFields(jobExecutionContext.getJobInstance());
}
}
@Override
public void jobExecutionVetoed(JobExecutionContext context)
{
stopStartedScopes();
}
@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException)
{
stopStartedScopes();
}
private void stopStartedScopes()
{
JobListenerContext jobListenerContext = currentJobListenerContext.get();
if (jobListenerContext != null)
{
jobListenerContext.stopStartedScopes();
currentJobListenerContext.set(null);
currentJobListenerContext.remove();
}
}
}
private static class JobListenerContext
{
private Stack> scopes = new Stack>();
private DependentProvider contextControl;
public void startContexts(Scheduled scheduled)
{
Collections.addAll(this.scopes, scheduled.startScopes());
if (!this.scopes.isEmpty())
{
this.contextControl = BeanProvider.getDependent(ContextControl.class);
for (Class extends Annotation> scopeAnnotation : this.scopes)
{
contextControl.get().startContext(scopeAnnotation);
}
}
}
private void stopStartedScopes()
{
if (this.contextControl == null)
{
return;
}
while (!this.scopes.empty())
{
this.contextControl.get().stopContext(this.scopes.pop());
}
this.contextControl.destroy();
}
}
@Override
public S unwrap(Class extends S> schedulerClass)
{
if (schedulerClass.isAssignableFrom(this.scheduler.getClass()))
{
return (S)this.scheduler;
}
throw new IllegalArgumentException(schedulerClass.getName() +
" isn't compatible with " + this.scheduler.getClass().getName());
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy