/** * Copyright 2016-2017 Linagora, Université Joseph Fourier, Floralis * * The present code is developed in the scope of the joint LINAGORA - * Université Joseph Fourier - Floralis research program and is designated * as a "Result" pursuant to the terms and conditions of the LINAGORA * - Université Joseph Fourier - Floralis research program. Each copyright * holder of Results enumerated here above fully & independently holds complete * ownership of the complete Intellectual Property rights applicable to the whole * of said Results, and may freely exploit it in any manner which does not infringe * the moral rights of the other copyright holders. * * Licensed 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 net.roboconf.dm.scheduler.internal; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Properties; import java.util.UUID; import java.util.logging.Logger; import org.quartz.CronScheduleBuilder; import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.TriggerKey; import org.quartz.impl.StdSchedulerFactory; import net.roboconf.core.Constants; import net.roboconf.core.model.beans.Application; import net.roboconf.core.model.runtime.ScheduledJob; import net.roboconf.core.utils.Utils; import net.roboconf.dm.management.Manager; import net.roboconf.dm.management.events.IDmListener; import net.roboconf.dm.scheduler.IScheduler; /** * @author Vincent Zurczak - Linagora */ public class RoboconfScheduler implements IScheduler { static final String JOB_ID = "id"; static final String JOB_NAME = "job-name"; static final String APP_NAME = "application-name"; static final String CMD_NAME = "command-file-name"; static final String CRON = "cron"; static final String MANAGER = "manager"; static final String PROJECT_DIR_SCHEDULER = "scheduler"; private final Logger logger = Logger.getLogger( getClass().getName()); IDmListener dmListener; Scheduler scheduler; Manager manager; /** * Starts the scheduler. * <p> * Invoked by iPojo. * </p> */ public void start() throws Exception { this.logger.info( "Roboconf's scheduler is starting..." ); // Verify the "scheduler" directory exists File schedulerDirectory = getSchedulerDirectory(); Utils.createDirectory( schedulerDirectory ); // Disable Quartz update checks StringBuilder quartzProperties = new StringBuilder(); quartzProperties.append( "org.quartz.scheduler.instanceName: Roboconf Quartz Scheduler\n" ); quartzProperties.append( "org.quartz.threadPool.threadCount = 3\n" ); quartzProperties.append( "org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore\n" ); quartzProperties.append( "org.quartz.scheduler.skipUpdateCheck: false\n" ); StdSchedulerFactory factory = new StdSchedulerFactory(); factory.initialize( new ByteArrayInputStream( quartzProperties.toString().getBytes( "UTF-8" ))); // Create a new scheduler this.scheduler = factory.getScheduler(); this.scheduler.start(); this.scheduler.getContext().put( MANAGER, this.manager ); // Add a listener to the DM this.dmListener = new ManagerListener( this ); this.manager.listenerAppears( this.dmListener ); // Reload all the jobs loadJobs(); } /** * Stops the scheduler. * <p> * Invoked by iPojo. * </p> */ public void stop() throws Exception { this.logger.info( "Roboconf's scheduler is stopping..." ); // Remove the DM listener if( this.manager != null ) { this.manager.listenerDisappears( this.dmListener ); this.dmListener = null; } // Shutdown the scheduler if( this.scheduler != null ) { this.scheduler.shutdown(); this.scheduler = null; } } /** * @param manager the manager to set (to be used outside OSGi environments) */ public void setManager( Manager manager ) { this.manager = manager; } @Override public void loadJobs() { this.logger.fine( "Roboconf's scheduler is loading jobs..." ); for( File f : Utils.listAllFiles( getSchedulerDirectory(), Constants.FILE_EXT_PROPERTIES )) { try { Properties props = Utils.readPropertiesFileQuietly( f, this.logger ); // Inject the ID in the properties props.setProperty( JOB_ID, Utils.removeFileExtension( f.getName())); // Validate and reload if( validProperties( props )) scheduleJob( props ); else this.logger.warning( "Skipped schedule for a job. There are invalid or missing job properties in " + f.getName()); } catch( Exception e ) { // Catch ALL the exceptions. #start() cannot fail. this.logger.warning( "Failed to load a scheduled job from " + f.getName()); Utils.logException( this.logger, e ); } } } @Override public List<ScheduledJob> listJobs() { this.logger.fine( "Roboconf's scheduler is listing jobs..." ); List<ScheduledJob> result = new ArrayList<> (); for( File f : Utils.listAllFiles( getSchedulerDirectory(), Constants.FILE_EXT_PROPERTIES )) { Properties props = Utils.readPropertiesFileQuietly( f, this.logger ); if( props.isEmpty()) continue; // Inject the ID in the properties props.put( JOB_ID, Utils.removeFileExtension( f.getName())); ScheduledJob job = from( props ); result.add( job ); } Collections.sort( result ); return result; } @Override public String saveJob( String jobId, String jobName, String cmdName, String cron, String appName ) throws IOException, IllegalArgumentException { // Create the job properties Properties props = new Properties(); if( jobId == null ) jobId = UUID.randomUUID().toString(); if( jobName != null ) props.setProperty( JOB_NAME, jobName ); if( cmdName != null ) props.setProperty( CMD_NAME, cmdName ); if( appName != null ) props.setProperty( APP_NAME, appName ); if( cron != null ) props.setProperty( CRON, cron ); // Validate... String result = null; if( validProperties( props )) { this.logger.fine( "Roboconf's scheduler is about to save a job as " + jobName ); // Verify the parameters Application app = this.manager.applicationMngr().findApplicationByName( appName ); if( app == null ) throw new IllegalArgumentException( appName + " does not exist" ); if( ! this.manager.commandsMngr().listCommands( app ).contains( cmdName )) throw new IllegalArgumentException( "Command " + cmdName + " does not exist" ); // Unschedule the job, if any unscheduleJob( jobId ); try { // Inject the ID in the properties and schedule the job props.setProperty( JOB_ID, jobId ); scheduleJob( props ); result = jobId; // Save the job's information props.remove( JOB_ID ); Utils.createDirectory( getSchedulerDirectory()); Utils.writePropertiesFile( props, getJobFile( jobId )); this.logger.fine( "Roboconf's scheduler has just saved a job as " + jobName ); } catch( Exception e ) { throw new IOException( e ); } } return result; } @Override public void deleteJob( String jobId ) throws IOException { this.logger.fine( "Roboconf's scheduler is about to delete a job with ID " + jobId ); try { unscheduleJob( jobId ); } catch( IOException e ) { this.logger.warning( "Failed to remove a scheduled job. Job's id: " + jobId ); throw e; } } @Override public ScheduledJob findJobProperties( String jobId ) { this.logger.fine( "Roboconf's scheduler is about to find the properties of the job whose ID is " + jobId ); ScheduledJob result = null; File f = getJobFile( jobId ); if( f.isFile()) { // Inject the ID in the properties Properties props = Utils.readPropertiesFileQuietly( f, this.logger ); props.put( JOB_ID, jobId ); result = from( props ); } return result; } File getSchedulerDirectory() { return new File( this.manager.configurationMngr().getWorkingDirectory(), PROJECT_DIR_SCHEDULER ); } File getJobFile( String jobId ) { return new File( getSchedulerDirectory(), jobId + Constants.FILE_EXT_PROPERTIES ); } /** * @param props non-null properties * @return true if the properties are valid */ boolean validProperties( Properties props ) { // We do not consider the job ID here. // Job ID = file name. No need to duplicate the information as // it would imply coherence checking. String jobName = props.getProperty( JOB_NAME, "" ); String appName = props.getProperty( APP_NAME, "" ); String cmdName = props.getProperty( CMD_NAME, "" ); String cron = props.getProperty( CRON, "" ); return ! Utils.isEmptyOrWhitespaces( cron ) && ! Utils.isEmptyOrWhitespaces( jobName ) && ! Utils.isEmptyOrWhitespaces( appName ) && ! Utils.isEmptyOrWhitespaces( cmdName ); } /** * @param props non-null and VALID properties * @return true if the job properties are correct and the job was successfully scheduled * @throws IOException * @see {@link #validProperties(Properties)} */ private void scheduleJob( Properties props ) throws Exception { // 1 file = 1 job = 1 trigger. String jobId = props.getProperty( JOB_ID, "" ); String jobName = props.getProperty( JOB_NAME, "" ); String appName = props.getProperty( APP_NAME, "" ); String cmdName = props.getProperty( CMD_NAME, "" ); String cron = props.getProperty( CRON, "" ); // Schedule the job JobDetail job = JobBuilder.newJob( CommandExecutionJob.class ) .withIdentity( jobId, appName ) .usingJobData( JOB_ID, jobId ) .usingJobData( APP_NAME, appName ) .usingJobData( JOB_NAME, jobName ) .usingJobData( CMD_NAME, cmdName ) .build(); Trigger trigger = TriggerBuilder .newTrigger() .withIdentity( jobId, appName ) .withSchedule( CronScheduleBuilder.cronSchedule( cron )) .build(); this.scheduler.scheduleJob( job, trigger ); } private void unscheduleJob( String jobId ) throws IOException { File f = getJobFile( jobId ); try { if( f.exists()) { Properties props = Utils.readPropertiesFileQuietly( f, this.logger ); String appName = props.getProperty( APP_NAME, "" ); if( ! Utils.isEmptyOrWhitespaces( appName )) this.scheduler.unscheduleJob( TriggerKey.triggerKey( jobId, appName )); } } catch( SchedulerException e ) { // Catch all the exceptions (including the runtime ones) throw new IOException( e ); } finally { Utils.deleteFilesRecursively( f ); } } private ScheduledJob from( Properties props ) { ScheduledJob job = new ScheduledJob( props.getProperty( JOB_ID )); job.setAppName( props.getProperty( APP_NAME )); job.setCmdName( props.getProperty( CMD_NAME )); job.setJobName( props.getProperty( JOB_NAME )); job.setCron( props.getProperty( CRON )); return job; } }