/* * 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<T> implements Scheduler<T> { private static final Logger LOG = Logger.getLogger(AbstractQuartzScheduler.class.getName()); private static final Scheduled DEFAULT_SCHEDULED_LITERAL = AnnotationInstanceProvider.of(Scheduled.class); private static ThreadLocal<JobListenerContext> currentJobListenerContext = new ThreadLocal<JobListenerContext>(); 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<String> 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<Class<? extends Annotation>> scopes = new Stack<Class<? extends Annotation>>(); private DependentProvider<ContextControl> 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> 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()); } }