package de.chandre.quartz.spring; import java.io.IOException; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Properties; import javax.sql.DataSource; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.quartz.JobListener; import org.quartz.Scheduler; import org.quartz.SchedulerListener; import org.quartz.Trigger; import org.quartz.TriggerListener; import org.quartz.impl.SchedulerRepository; import org.quartz.spi.JobFactory; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.quartz.SchedulerFactoryBean; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import de.chandre.quartz.spring.QuartzSchedulerProperties.Persistence; import de.chandre.quartz.spring.QuartzSchedulerProperties.SchedulerFactory; import de.chandre.quartz.spring.listener.TriggerMetricsListener; /** * Spring-Boot auto-configuration for Quartz-Scheduler * @author André Hertwig * @since 1.0.0 */ @Configuration @EnableConfigurationProperties(QuartzSchedulerProperties.class) @ConditionalOnClass(Scheduler.class) public class QuartzSchedulerAutoConfiguration { private static final Log LOGGER = LogFactory.getLog(QuartzSchedulerAutoConfiguration.class); public static final String QUARTZ_PROPERTIES_BEAN_NAME = "quartzProperties"; public static final String QUARTZ_SCHEDULER_FACTORY_BEAN_NAME = "autoSchedulerFactory"; public static final String QUARTZ_JOB_FACTORY_BEAN_NAME = "autoJobFactory"; public static final String QUARTZ_SCHEDULER_METRICS_LISTENER_BEAN_NAME = "quartzMetricsListener"; @Configuration @ConditionalOnProperty(prefix = QuartzSchedulerProperties.PREFIX, name = "enabled", havingValue="true", matchIfMissing = true) @ConditionalOnMissingBean(name = QUARTZ_SCHEDULER_FACTORY_BEAN_NAME) protected static class SchedulerFactoryConfiguration { private static Collection<Trigger> getTriggers(ApplicationContext applicationContext) { Map<String, Trigger> triggers = applicationContext.getBeansOfType(Trigger.class); if (null != triggers && !triggers.isEmpty()) { return triggers.values(); } return null; } private static PlatformTransactionManager getTransactionManager(ApplicationContext applicationContext, String txManagerBeanName) { Map<String, PlatformTransactionManager> txManagers = applicationContext.getBeansOfType(PlatformTransactionManager.class); if (null != txManagers && txManagers.size() > 0) { if (txManagers.size() == 1) { LOGGER.debug("only one txManager found, returning: " + txManagers.keySet().iterator().next()); return txManagers.values().iterator().next(); } else if (!StringUtils.isEmpty(txManagerBeanName)) { LOGGER.debug("more than one txManager found, try using: " + txManagerBeanName); PlatformTransactionManager txManager = txManagers.get(txManagerBeanName); if (null == txManager) { LOGGER.warn("QuartzSchedulerAutoConfiguration is configured to use " + txManagerBeanName + " as PlatformTransactionManager, but no bean for this name has been found in context!"); } return txManager; } else { LOGGER.warn("QuartzSchedulerAutoConfiguration is configured to use PlatformTransactionManager, " + "but more than one has been found in context! " + "Consider using quartz.persistence.platform-tx-manager-bean-name in pallication configuration."); } } else { LOGGER.warn("QuartzSchedulerAutoConfiguration is configured to use PlatformTransactionManager, " + "but no bean of this type has been found in context!"); } return null; } private static DataSource getDataSource(ApplicationContext applicationContext, Persistence persistenceSettings) { DataSource dataSource = null; Map<String, DataSource> datasources = applicationContext.getBeansOfType(DataSource.class); int dsSize = null != datasources ? datasources.size() : 0; if (null != datasources && null != persistenceSettings.getDataSourceName()) { dataSource = datasources.get(persistenceSettings.getDataSourceName()); } else if (null != datasources && dsSize == 1 && null == persistenceSettings.getDataSourceName()){ dataSource = datasources.values().iterator().next(); } if (dataSource == null) { throw new BeanInitializationException( "A datasource is required when starting Quartz-Scheduler in persisted mode. " + "No DS found in map with size: " + dsSize + ", and configured DSName: " + persistenceSettings.getDataSourceName()); } return dataSource; } private static QuartzSchedulerFactoryOverrideHook getQuartzSchedulerFactoryOverrideHook(ApplicationContext applicationContext) { try { return applicationContext.getBean(QuartzSchedulerFactoryOverrideHook.class); } catch (Exception e) { LOGGER.info("no QuartzSchedulerFactoryOverrideHook configured"); LOGGER.trace(e.getMessage(), e); } return null; } @Bean(name = QUARTZ_JOB_FACTORY_BEAN_NAME) @ConditionalOnMissingBean(name = QUARTZ_JOB_FACTORY_BEAN_NAME) public JobFactory autoJobFactory(ApplicationContext applicationContext, @Autowired(required=false) QuartzSchedulerProperties properties) { if (null == properties) { LOGGER.warn("no QuartzSchedulerProperties found, consider to set quartz.enabled=true in properties"); return null; } AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory(); jobFactory.setApplicationContext(applicationContext); return jobFactory; } @Bean(name = QUARTZ_PROPERTIES_BEAN_NAME) @ConditionalOnMissingBean(name = QUARTZ_PROPERTIES_BEAN_NAME) public Properties quartzProperties(ApplicationContext applicationContext, @Autowired(required=false) QuartzSchedulerProperties properties) throws IOException { if (null == properties) { LOGGER.warn("no QuartzSchedulerProperties found, consider to set quartz.enabled=true in properties"); return null; } Properties quartzProperties = null; if (properties.isOverrideConfigLocationProperties()) { //merge properties from file with springs application properties quartzProperties = loadConfigLocationProperties(applicationContext, properties); quartzProperties.putAll(properties.getProperties()); } else if (null != properties.getProperties() && !properties.getProperties().isEmpty()) { // only use the spring application properties quartzProperties = getConfiguredProperties(properties); } else { // only use the properties from file quartzProperties = loadConfigLocationProperties(applicationContext, properties); } //Call the override hook to possibly change runtime data QuartzPropertiesOverrideHook hook = getQuartzPropOverrideHook(applicationContext); if (null != hook) { quartzProperties = hook.override(quartzProperties); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Quartz-Properties"); quartzProperties.entrySet().forEach(entry -> { LOGGER.debug(" " + entry.getKey() + " = " + entry.getValue()); }); } return quartzProperties; } private static Properties getConfiguredProperties(QuartzSchedulerProperties properties) { Properties quartzProperties = new Properties(); quartzProperties.putAll(properties.getProperties()); return quartzProperties; } private static Properties loadConfigLocationProperties(ApplicationContext applicationContext, QuartzSchedulerProperties properties) throws IOException { String location = properties.getPropertiesConfigLocation(); if(null == location || location.trim().length() == 0) { location = QuartzSchedulerProperties.DEFAULT_CONFIG_LOCATION; LOGGER.debug("using default 'quartz.properties' from classpath: " + location); } else { LOGGER.debug("using 'quartz.properties' from location: " + location); } PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean(); propertiesFactoryBean.setLocation(applicationContext.getResource(location)); propertiesFactoryBean.afterPropertiesSet(); return propertiesFactoryBean.getObject(); } private static QuartzPropertiesOverrideHook getQuartzPropOverrideHook(ApplicationContext applicationContext) { try { return applicationContext.getBean(QuartzPropertiesOverrideHook.class); } catch (Exception e) { LOGGER.info("no QuartzPropertiesOverrideHook configured"); LOGGER.trace(e.getMessage(), e); } return null; } @Bean(name = QUARTZ_SCHEDULER_FACTORY_BEAN_NAME) @ConditionalOnMissingBean public SchedulerFactoryBean autoSchedulerFactory(ApplicationContext applicationContext, JobFactory jobFactory, @Autowired(required=false) QuartzSchedulerProperties properties, @Qualifier(QUARTZ_PROPERTIES_BEAN_NAME) Properties quartzProperties, @Autowired(required=false) List<TriggerListener> triggerListeners, @Autowired(required=false) List<JobListener> jobListeners, @Autowired(required=false) List<SchedulerListener> schedulerListeners) { if (null == properties) { LOGGER.warn("no QuartzSchedulerProperties found, consider to set quartz.enabled=true in properties"); return null; } LOGGER.debug("creating SchedulerFactory"); SchedulerFactory factorySettings = properties.getSchedulerFactory(); SchedulerRepository schedulerRepo = SchedulerRepository.getInstance(); if (schedulerRepo.remove(QUARTZ_SCHEDULER_FACTORY_BEAN_NAME)) { LOGGER.debug("removed scheduler from SchedulerRepository with name: " + QUARTZ_SCHEDULER_FACTORY_BEAN_NAME); } if (null != factorySettings.getSchedulerName() && schedulerRepo.remove(factorySettings.getSchedulerName())) { LOGGER.debug("removed scheduler from SchedulerRepository with name: " + factorySettings.getSchedulerName()); } SchedulerFactoryBean factory = BeanUtils.instantiateClass(SchedulerFactoryBean.class); factory.setApplicationContext(applicationContext); factory.setJobFactory(jobFactory); Persistence persistenceSettings = properties.getPersistence(); if (persistenceSettings.isPersisted()) { factory.setDataSource(getDataSource(applicationContext, persistenceSettings)); if (persistenceSettings.isUsePlatformTxManager()) { PlatformTransactionManager txManager = getTransactionManager(applicationContext, persistenceSettings.getPlatformTxManagerBeanName()); if (null != txManager) { factory.setTransactionManager(txManager); } } } if (!StringUtils.isEmpty(factorySettings.getSchedulerName())) { factory.setSchedulerName(factorySettings.getSchedulerName()); } else { LOGGER.debug("no SchedulerName configured, using bean name: " + QUARTZ_SCHEDULER_FACTORY_BEAN_NAME); } factory.setPhase(factorySettings.getPhase()); factory.setStartupDelay(factorySettings.getStartupDelay()); factory.setAutoStartup(factorySettings.isAutoStartup()); factory.setWaitForJobsToCompleteOnShutdown(factorySettings.isWaitForJobsToCompleteOnShutdown()); factory.setOverwriteExistingJobs(factorySettings.isOverwriteExistingJobs()); factory.setExposeSchedulerInRepository(factorySettings.isExposeSchedulerInRepository()); factory.setQuartzProperties(quartzProperties); if (!CollectionUtils.isEmpty(jobListeners)) { LOGGER.info("configuring " + jobListeners.size() + " job listeners"); factory.setGlobalJobListeners(jobListeners.toArray(new JobListener[]{})); } if (!CollectionUtils.isEmpty(triggerListeners)) { LOGGER.info("configuring " + triggerListeners.size() + " trigger listeners"); factory.setGlobalTriggerListeners(triggerListeners.toArray(new TriggerListener[]{})); } if (!CollectionUtils.isEmpty(schedulerListeners)) { LOGGER.info("configuring " + schedulerListeners.size() + " scheduler listeners"); factory.setSchedulerListeners(schedulerListeners.toArray(new SchedulerListener[]{})); } Collection<Trigger> triggers = getTriggers(applicationContext); if (null != triggers && !triggers.isEmpty()) { factory.setTriggers(triggers.toArray(new Trigger[triggers.size()])); LOGGER.info("staring scheduler factory with " + triggers.size() + " job triggers"); } else { LOGGER.info("staring scheduler factory with 0 job triggers"); } QuartzSchedulerFactoryOverrideHook hook = getQuartzSchedulerFactoryOverrideHook(applicationContext); if (null != hook) { factory = hook.override(factory, properties, quartzProperties); } return factory; } } @Configuration @ConditionalOnProperty(prefix = QuartzSchedulerProperties.PREFIX+".metrics", name = "enabled", havingValue="true", matchIfMissing = false) @ConditionalOnMissingBean(name = QUARTZ_SCHEDULER_METRICS_LISTENER_BEAN_NAME) @AutoConfigureBefore(name=QUARTZ_SCHEDULER_FACTORY_BEAN_NAME) protected static class SchedulerMetricsListenerConfiguration { @Bean(name = QUARTZ_SCHEDULER_METRICS_LISTENER_BEAN_NAME) @ConditionalOnMissingBean public TriggerMetricsListener schedulerMetricsListener(@Autowired(required=false) QuartzSchedulerProperties properties) { if (null == properties) { LOGGER.warn("no QuartzSchedulerProperties found, consider to set quartz.enabled=true in properties"); return null; } TriggerMetricsListener listener = new TriggerMetricsListener(properties.getMetrics(), properties.getMetrics().getListenerName()); return listener; } } }