All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sakaiproject.component.app.scheduler.SchedulerManagerImpl Maven / Gradle / Ivy

There is a newer version: 23.3
Show newest version
/**********************************************************************************
 * $URL$
 * $Id$
 **********************************************************************************
 *
 * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation
 *
 * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.component.app.scheduler;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.quartz.CronScheduleBuilder;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.JobListener;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerListener;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.GroupMatcher;
import org.sakaiproject.api.app.scheduler.ConfigurableJobProperty;
import org.sakaiproject.api.app.scheduler.ConfigurableJobPropertyValidationException;
import org.sakaiproject.api.app.scheduler.ConfigurableJobPropertyValidator;
import org.sakaiproject.api.app.scheduler.JobBeanWrapper;
import org.sakaiproject.api.app.scheduler.SchedulerManager;
import org.sakaiproject.component.cover.ComponentManager;
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.component.app.scheduler.jobs.SpringConfigurableJobBeanWrapper;
import org.sakaiproject.component.app.scheduler.jobs.SpringInitialJobSchedule;
import org.sakaiproject.component.app.scheduler.jobs.SpringJobBeanWrapper;
import org.sakaiproject.db.api.SqlService;

public class SchedulerManagerImpl implements SchedulerManager
{

  private static final Logger LOG = LoggerFactory.getLogger(SchedulerManagerImpl.class);
  public final static String
        SCHEDULER_LOADJOBS      = "scheduler.loadjobs";
  private DataSource dataSource;
  private String serverId;
  private Set qrtzJobs;
  private Map qrtzQualifiedJobs = new TreeMap(); // map for SelectItems
  /** The properties file from the classpath */
  private String qrtzPropFile;
  /** The properties file from sakai.home */
  private String qrtzPropFileSakai;
  private Properties qrtzProperties;
  private TriggerListener globalTriggerListener;
  private Boolean autoDdl;
  private boolean startScheduler = true;
  private Map beanJobs = new Hashtable();

  private static final String JOB_INTERFACE = "org.quartz.Job";
  private static final String STATEFULJOB_INTERFACE = "org.quartz.StatefulJob";
  

  // Service dependencies
  private ServerConfigurationService serverConfigurationService;
  private SchedulerFactory schedFactory;
  private Scheduler scheduler;
  private SqlService sqlService;


  private LinkedList
      globalTriggerListeners = new LinkedList();
  private LinkedList
      globalJobListeners = new LinkedList();

  private LinkedList
      initialJobSchedule = null;

public void init()
  {
    try
    {
      qrtzProperties = initQuartzConfiguration();

      qrtzProperties.setProperty("org.quartz.scheduler.instanceId", serverId);

      // note: becuase job classes are jarred , it is impossible to iterate
      // through a directory by calling listFiles on a file object.
      // Therefore, we need the class list list from spring.

      // find quartz jobs from specified 'qrtzJobs' and verify they
      // that these jobs implement the Job interface
      Iterator qrtzJobsIterator = qrtzJobs.iterator();
      while (qrtzJobsIterator.hasNext())
      {
        String className = (String) qrtzJobsIterator.next();
        Class cl = null;
        try
        {
          cl = Class.forName(className);
        }
        catch (ClassNotFoundException e)
        {
          LOG.warn("Could not locate class: " + className + " on classpath");
        }
        if (cl != null)
        {
          // check that each class implements the Job interface           
          if (doesImplementJobInterface(cl))
          {
            qrtzQualifiedJobs.put(cl.getName(), cl.getName());
          }
          else
          {
            LOG.warn("Class: " + className
                + " does not implement quartz Job interface");
          }
        }
      }
   // run ddl
      if (autoDdl.booleanValue()){
        try
        {
           sqlService.ddl(this.getClass().getClassLoader(), "quartz2");
        }
        catch (Throwable t)
        {
          LOG.warn(this + ".init(): ", t);
        }
      }

      boolean isInitialStartup = isInitialStartup(sqlService);
      if (isInitialStartup && autoDdl.booleanValue())
      {
    	  LOG.info("Performing initial population of the Quartz tables.");
    	  sqlService.ddl(this.getClass().getClassLoader(), "init_locks2");
      }
      /*
         Determine whether or not to load the jobs defined in the initialJobSchedules list. These jobs will be loaded
         under the following conditions:
            1) the server configuration property "scheduler.loadjobs" is "true"
            2) "scheduler.loadjobs" is "init" and this is the first startup for the scheduler (eg. this is a new Sakai instance)
         "scheduler.loadjobs" is set to "init" by default
       */
      String
          loadJobs = serverConfigurationService.getString(SCHEDULER_LOADJOBS, "init").trim();

      List
          initSchedules = getInitialJobSchedules();
      
      boolean
          loadInitSchedules = (initSchedules != null) && (initSchedules.size() > 0) &&
                                (("init".equalsIgnoreCase(loadJobs) && isInitialStartup) ||
                                 "true".equalsIgnoreCase(loadJobs));

      if (loadInitSchedules)
          LOG.debug ("Preconfigured jobs will be loaded");
      else
          LOG.debug ("Preconfigured jobs will not be loaded");
      
      


      // start scheduler and load jobs
      schedFactory = new StdSchedulerFactory(qrtzProperties);
      scheduler = schedFactory.getScheduler();

      // loop through persisted jobs removing both the job and associated
      // triggers for jobs where the associated job class is not found
      Set jobKeys = scheduler.getJobKeys(GroupMatcher.jobGroupEquals(Scheduler.DEFAULT_GROUP));
      for (JobKey key : jobKeys) {
        try
        {
          JobDetail detail = scheduler.getJobDetail(key);
          String bean = detail.getJobDataMap().getString(JobBeanWrapper.SPRING_BEAN_NAME);
          Job job = (Job) ComponentManager.get(bean);
          if (job == null) {
              LOG.warn("scheduler cannot load class for persistent job:" + key);
        	  scheduler.deleteJob(key);
              LOG.warn("deleted persistent job:" + key);
          }
        }
        catch (SchedulerException e)
        {
          LOG.warn("scheduler cannot load class for persistent job:" + key);
          scheduler.deleteJob(key);
          LOG.warn("deleted persistent job:" + key);
        }
      }

      for (TriggerListener tListener : globalTriggerListeners)
      {
          scheduler.getListenerManager().addTriggerListener(tListener);
      }

      for (JobListener jListener : globalJobListeners)
      {
          scheduler.getListenerManager().addJobListener(jListener);
      }

      if (loadInitSchedules)
      {
          LOG.debug ("Loading preconfigured jobs");
          loadInitialSchedules();
      }

      //scheduler.addGlobalTriggerListener(globalTriggerListener);
      if (isStartScheduler()) {
          scheduler.start();
      } else {
          LOG.info("Scheduler Not Started, startScheduler=false");
      }
    }
    catch (Exception e)
    {
      LOG.error("Failed to start scheduler.", e);
      throw new IllegalStateException("Scheduler cannot start!", e);
    }
  }

  /**
   * This loads the configurations for quartz.
   * It loads the defaults from the classpath and then loads override values from
   * sakai.home.
   * @return The quartz properties.
   * @throws IOException When we can't load the default values.
   */
  private Properties initQuartzConfiguration() throws IOException
  {
    InputStream propertiesInputStream = null;
    Properties properties = new Properties();
    // load the default quartz properties file
    // if this fails we want to propogate the error to stop startup.
    try
    {
      propertiesInputStream = this.getClass().getResourceAsStream(qrtzPropFile);
      properties.load(propertiesInputStream);
    }
    finally
    {
      if (propertiesInputStream != null)
      {
        try
        {
          propertiesInputStream.close();
        }
        catch (IOException e)
        {
          LOG.debug("Failed to close stream.", e);
        }
      }
    }
  
    // load the configuration out of sakai home
    // any failures here shouldn't result in startup failing.
    File file = new File(serverConfigurationService.getSakaiHomePath(), qrtzPropFileSakai);
    if (file.exists() && file.isFile()) {
      try
      {
        propertiesInputStream = new FileInputStream(file);
        properties.load(propertiesInputStream);
        LOG.info("Loaded extra configuration from: "+ file.getAbsolutePath());
      }
      catch (IOException e)
      {
        LOG.warn("Failed to load file: "+ file, e);
      }
      finally
      {
        if (propertiesInputStream != null)
        {
          try
          {
            propertiesInputStream.close();
          }
          catch (IOException e)
          {
            LOG.debug("Failed to close stream.", e);
          }
        }
      }
    }
    return properties;
  }

  private boolean doesImplementJobInterface(Class cl)
  {
    Class[] classArr = cl.getInterfaces();
    for (int i = 0; i < classArr.length; i++)
    {
    	if (classArr[i].getName().equals(JOB_INTERFACE) || 
    			classArr[i].getName().equals(STATEFULJOB_INTERFACE))
      {
        return true;
      }
    }
    return false;
  }

    /**
     * Runs an SQL select statement to determine if the Quartz lock rows exist in the database. If the rows do not exist
     * this method assumes this is the first time the scheduler has been started. The select statement will be defined
     * in the {vendor}/checkTables.sql file within the shared library deployed by this project. The statement should be
     * of the form "SELECT COUNT(*) from QUARTZ_LOCKS;". If the count is zero it is assumed this is a new install. 
     * If the count is non-zero it is assumed the QUARTZ_LOCKS table has been initialized and this is not a new install.
     *
     * @param sqlService
     * @return
     */
  private boolean isInitialStartup(SqlService sqlService)
  {
      String
          checkTablesScript = sqlService.getVendor() + "/checkTables.sql";
      ClassLoader
          loader = this.getClass().getClassLoader();
      String
          chkStmt = null;
      InputStream
          in = null;
      BufferedReader
          r = null;

      try
      {

          // find the resource from the loader
          in = loader.getResourceAsStream(checkTablesScript);

          r = new BufferedReader(new InputStreamReader(in));

          chkStmt = r.readLine();
      }
      catch (Exception e)
      {
          LOG.error("Could not read the file " + checkTablesScript + " to determine if this is a new installation. Preconfigured jobs will only be loaded if the server property scheduler.loadjobs is \"true\"", e);
          return false;
      }
      finally
      {
          try
          {
              r.close();
          }
          catch (Exception e){}
          try
          {
              in.close();
          }
          catch (Exception e){}
      }

      List l = sqlService.dbRead(chkStmt);
      if (l != null && l.size() > 0) 
      {
    	  return (l.get(0).equalsIgnoreCase("0"));
      }
      else 
      {
    	  return false;
      }
  }

    /**
     * Loads jobs and schedules triggers for preconfigured jobs.
     */
  private void loadInitialSchedules()
  {
      for (SpringInitialJobSchedule sched : getInitialJobSchedules())
      {
          SpringJobBeanWrapper
              wrapper = sched.getJobBeanWrapper();

          LOG.debug ("Loading schedule for preconfigured job \"" + wrapper.getJobType() + "\"");

          JobDetail jd = JobBuilder.newJob(wrapper.getJobClass())
                  .withIdentity(sched.getJobName(), Scheduler.DEFAULT_GROUP)
                  .storeDurably()
                  .requestRecovery()
                  .build();

          JobDataMap map = jd.getJobDataMap();

          map.put(JobBeanWrapper.SPRING_BEAN_NAME, wrapper.getBeanId());
          map.put(JobBeanWrapper.JOB_TYPE, wrapper.getJobType());

          if (SpringConfigurableJobBeanWrapper.class.isAssignableFrom(wrapper.getClass()))
          {
              SpringConfigurableJobBeanWrapper
                  confJob = (SpringConfigurableJobBeanWrapper) wrapper;
              ConfigurableJobPropertyValidator
                  validator = confJob.getConfigurableJobPropertyValidator();
              Map
                  conf = sched.getConfiguration();
              boolean
                  fail = false;

              for (ConfigurableJobProperty cProp : confJob.getConfigurableJobProperties())
              {
                  String
                      key = cProp.getLabelResourceKey(),
                      val = conf.get(key);

                  LOG.debug ("job property '" + key + "' is set to '" + val + "'");

                  if (val == null && cProp.isRequired())
                  {
                      val = cProp.getDefaultValue();

                      if (val == null)
                      {
                          LOG.error ("job property '" + key + "' is required but has no value; job '" + sched.getJobName() + "' of type '" + wrapper.getJobClass() + "' will not be configured");

                          fail = true;
                          break;
                      }

                      LOG.debug ("job property '" + key + "' set to default value '" + val + "'");
                  }

                  if (val != null)
                  {

                      try
                      {
                          validator.assertValid(key, val);
                      }
                      catch (ConfigurableJobPropertyValidationException cjpve)
                      {
                          LOG.error ("job property '" + key + "' was set to an invalid value '" + val + "'; job '" + sched.getJobName() + "' of type '" + wrapper.getJobClass() + "' will not be configured");

                          fail = true;
                          break;
                      }

                      map.put (key, val);
                  }

              }
              if (fail) continue;
          }

          try
          {
              scheduler.addJob(jd, false);
          }
          catch (SchedulerException e)
          {
              LOG.error ("Failed to schedule job '" + sched.getJobName() + "' of type '" + wrapper.getJobClass() + "'");
              continue;
          }

          Trigger trigger = null;
          trigger = TriggerBuilder.newTrigger()
                  .withIdentity(sched.getTriggerName(), Scheduler.DEFAULT_GROUP)
                  .forJob(jd.getKey())
                  .withSchedule(CronScheduleBuilder.cronSchedule(sched.getCronExpression()))
                  .build();

          try
          {
              scheduler.scheduleJob(trigger);
          }
          catch (SchedulerException e)
          {
              LOG.error ("Trigger could not be scheduled. Failed to schedule job '" + sched.getJobName() + "' of type '" + wrapper.getJobClass() + "'");
          }

      }
  }


  /**
   * @see org.sakaiproject.api.app.scheduler.SchedulerManager#destroy()
   */
  public void destroy()
  {
    try{
      if (!scheduler.isShutdown()){
        scheduler.shutdown();
      }
    }
    catch (Throwable t){
      LOG.error("An error occurred while stopping the scheduler", t);
    }
  }


  public List getInitialJobSchedules()
  {
      return initialJobSchedule;
  }

  public void setInitialJobSchedules(List jobSchedule)
  {
      if(jobSchedule == null || jobSchedule.size() < 1)
        return;
      
      this.initialJobSchedule = new LinkedList ();

      initialJobSchedule.addAll(jobSchedule);
  }
  /**
   * @deprecated use {@link #setGlobalTriggerListeners(Set)}
   * @return Returns the globalTriggerListener.
   */
  public TriggerListener getGlobalTriggerListener()
  {
    return globalTriggerListener;
  }

  /**
   * @deprecated use {@link #getGlobalTriggerListeners()}
   * @param globalTriggerListener The globalTriggerListener to set.
   */
  public void setGlobalTriggerListener(TriggerListener globalTriggerListener)
  {
    this.globalTriggerListener = globalTriggerListener;

      if (globalTriggerListeners != null)
      {
          globalTriggerListeners.addFirst(globalTriggerListener);
      }
  }

  public void setGlobalTriggerListeners (final List listeners)
  {
      globalTriggerListeners.clear();

      if (globalTriggerListener != null)
      {
          globalTriggerListeners.add(globalTriggerListener);
      }

      if (listeners != null)
      {
          globalTriggerListeners.addAll(listeners);
      }
  }

  public List getGlobalTriggerListeners()
  {
      return Collections.unmodifiableList(globalTriggerListeners);
  }

  public void setGlobalJobListeners (final List listeners)
  {
      globalJobListeners.clear();

      if (listeners != null)
      {
          globalJobListeners.addAll(listeners);
      }
  }

  public List getGlobalJobListeners()
  {
      return Collections.unmodifiableList(globalJobListeners);
  }

  /**
   * @return Returns the serverId.
   */
  public String getServerId()
  {
    return serverId;
  }

  /**
   * @param serverId The serverId to set.
   */
  public void setServerId(String serverId)
  {
    this.serverId = serverId;
  }

  /**
   * @return Returns the dataSource.
   */
  public DataSource getDataSource()
  {
    return dataSource;
  }

  /**
   * @param dataSource The dataSource to set.
   */
  public void setDataSource(DataSource dataSource)
  {
    this.dataSource = dataSource;
  }

  public void setSqlService(SqlService sqlService)
  {
    this.sqlService = sqlService;
  }

  /**
   * @return Returns the qrtzQualifiedJobs.
   */
  public Map getQrtzQualifiedJobs()
  {
    return qrtzQualifiedJobs;
  }

  /**
   * @param qrtzQualifiedJobs The qrtzQualifiedJobs to set.
   */
  public void setQrtzQualifiedJobs(Map qrtzQualifiedJobs)
  {
    this.qrtzQualifiedJobs = qrtzQualifiedJobs;
  }

  /**
   * @return Returns the qrtzJobs.
   */
  public Set getQrtzJobs()
  {
    return qrtzJobs;
  }

  /**
   * @param qrtzJobs The qrtzJobs to set.
   */
  public void setQrtzJobs(Set qrtzJobs)
  {
    this.qrtzJobs = qrtzJobs;
  }

  /**
   * @return Returns the qrtzPropFile.
   */
  public String getQrtzPropFile()
  {
    return qrtzPropFile;
  }

  /**
   * @param qrtzPropFile The qrtzPropFile to set.
   */
  public void setQrtzPropFile(String qrtzPropFile)
  {
    this.qrtzPropFile = qrtzPropFile;
  }

  public void setQrtzPropFileSakai(String qrtzPropFileSakai)
  {
    this.qrtzPropFileSakai = qrtzPropFileSakai;
  }

  /**
   * @return Returns the scheduler.
   */
  public Scheduler getScheduler()
  {
    return scheduler;
  }

  /**
   * @param scheduler The sched to set.
   */
  public void setScheduler(Scheduler scheduler)
  {
    this.scheduler = scheduler;
  }

  /**
   * @param serverConfigurationService The ServerConfigurationService to get our configuation from.
   */
  public void setServerConfigurationService(ServerConfigurationService serverConfigurationService)
  {
    this.serverConfigurationService = serverConfigurationService;
  }

  /**
   * @see org.sakaiproject.api.app.scheduler.SchedulerManager#setAutoDdl(java.lang.Boolean)
   */
  public void setAutoDdl(Boolean b)
  {
    autoDdl = b;
  }

   public Map getBeanJobs() {
      return beanJobs;
   }

   public void registerBeanJob(String jobName, JobBeanWrapper job) {
      getBeanJobs().put(jobName, job);
   }

   public JobBeanWrapper getJobBeanWrapper(String beanWrapperId) {
      return (JobBeanWrapper) getBeanJobs().get(beanWrapperId);
   }

   public boolean isStartScheduler() {
       return startScheduler;
   }

   public void setStartScheduler(boolean startScheduler) {
       this.startScheduler = startScheduler;
   }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy