/********************************************************************
 * Copyright © 2004 VHA. All rights reserved
 ********************************************************************/
// Package
package gov.va.med.fw.scheduling;

// Libraries class
import org.apache.commons.lang.Validate;
import org.quartz.JobDetail;
import org.quartz.JobPersistenceException;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.springframework.scheduling.quartz.JobDetailAwareTrigger;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import gov.va.med.fw.service.AbstractComponent;

/** 
 * A typical usage of this class is to register triggers and job details 
 * defined in ejb modules and and war modules using one common singleton 
 * scheduler defined at the EAR level. For instance, in ESR, a common 
 * scheduler is defined in a common.jar and scheduler post processor beans 
 * are defined in UI and Messaging to allow UI and Messaging to register
 * their own triggers, and job details.
 * 
 * <p>Note that triggers that are currently paused in the JobStore will NOT be
 * overwritten.
 *  
 * @author DNS   LEV
 */
public class SchedulerConfigurator extends AbstractComponent {

	/**
	 * An instance of a singleton scheduler
	 */
	private Scheduler scheduler = null;
	
	/**
	 * A list of triggers to add to a scheduler
	 */
	private Trigger[] triggers = null;
	
	/**
	 * A list of job details to add to a scheduler
	 */
	private JobDetail[] jobDetails = null;
	
	/** 
	 * A flag to overwrite existing jobs in the scheduler
	 * An instance of overwriteExistingJobs
	 */
	private boolean overwriteExistingJobs = true;
	
	/**
	 * Similar paradigm for Spring's SchedulerFactoryBean.  All operations to modify database
	 * triggers/jobs need to be wrapped in a Transaction so cluster race conditions do not occur.
	 * 
	 * #see org.springframework.scheduling.quartz.SchedulerFactoryBean#registerJobsAndTriggers
	 */
	private PlatformTransactionManager transactionManager;
	
	/**
	 * A default constructor 
	 */
	public SchedulerConfigurator() {
		super();
	}

	/** 
	 * Sets a flag to overwrite existing jobs in the scheduler
	 * @return Returns the overwriteExistingJobs.
	 */
	public boolean isOverwriteExistingJobs() {
		return overwriteExistingJobs;
	}

	/** 
	 * Returns a flag indicating whether overwriting jobs is desired
	 * @param overwriteExistingJobs The overwriteExistingJobs to set.
	 */
	public void setOverwriteExistingJobs( boolean overwriteExistingJobs ) {
		this.overwriteExistingJobs = overwriteExistingJobs;
	}

	/** 
	 * An instance of a singleton scheduler 
	 * @param scheduler The singleton scheduler to set.
	 */
	public void setScheduler( Scheduler scheduler ) {
		this.scheduler = scheduler;
	}

	/**
	 * A list of triggers to register to a scheduler at start-up
	 * @param triggers The list of triggers to set.
	 */
	public void setTriggers(Trigger[] triggers) {
		this.triggers = triggers;
	}
	
	/** 
	 * A list of job details to register to a scheduler at start-up
	 * @param jobDetails The list of job details to set
	 */
	public void setJobDetails( JobDetail[] jobDetails ) {
		this.jobDetails = jobDetails;
	}
	
	/** Registers a list of triggers and job details to a scheduler at start-up
	 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
	 */
	public void afterPropertiesSet() throws Exception {
		super.afterPropertiesSet();
		Validate.notNull( this.scheduler, "A scheduler is required" );
		
		TransactionStatus transactionStatus = null;
		if (this.transactionManager != null) {
			transactionStatus = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
		}
		try {
			// Register a list of triggers to a scheduler
			if( triggers != null && triggers.length > 0 ) {
				scheduleTriggers( triggers );
			}
	
			// Register a list of job details to a scheduler
			if( jobDetails != null && jobDetails.length > 0 ) {
				scheduleJobDetails( jobDetails );
			}
		} catch (Throwable ex) {
			if (transactionStatus != null) {
				try {
					this.transactionManager.rollback(transactionStatus);
				}
				catch (TransactionException tex) {
					logger.error("Job registration exception overridden by rollback exception", ex);
					throw tex;
				}
			}
			if (ex instanceof SchedulerException) {
				throw (SchedulerException) ex;
			}
			if (ex instanceof Exception) {
				throw new SchedulerException(
						"Registration of jobs and triggers failed: " + ex.getMessage(), (Exception) ex);
			}
			throw new SchedulerException("Registration of jobs and triggers failed: " + ex.getMessage());
		}
		if (transactionStatus != null) {
			this.transactionManager.commit(transactionStatus);
		}		
	}

	/** 
	 * Schedule all registered triggers to a scheduler
	 * @param triggers A list of triggers to schedule
	 */
	protected void scheduleTriggers( Trigger[] triggers ) throws SchedulerException {
		Trigger trigger = null;
		JobDetail jobDetail = null;
		
		// Loop through a list of triggers to register
		for( int i=0; i<triggers.length; i++ ) {
			trigger = triggers[i];

			// Check if the Trigger is aware of an associated JobDetail.
			jobDetail = trigger instanceof JobDetailAwareTrigger ?
					((JobDetailAwareTrigger)trigger).getJobDetail() : null;
			
			// Check if a trigger exists
			Trigger existingTrigger = this.scheduler.getTrigger( trigger.getName(), trigger.getGroup() );
			int existingTriggerStatus = this.scheduler.getTriggerState(trigger.getName(), trigger.getGroup());
			if( existingTrigger == null ) {		
				// no trigger in JobStore......add Trigger and JobDetail (if present)
				
				if( jobDetail != null ) {
					this.addJobDetailToScheduler( jobDetail );
				}
				
				try {
					this.scheduler.scheduleJob( trigger );
				}
				catch( JobPersistenceException e ) {
					if( logger.isDebugEnabled() ) {
						logger.debug( "Unexpectedly found existing job, assumably due to cluster race condition", e );
					}
					if( this.overwriteExistingJobs && !(trigger instanceof ImmutableInitTrigger) ) {
						this.scheduler.rescheduleJob( trigger.getName(), trigger.getGroup(), trigger );
					}
				}
			}
			else {
				// yes trigger in JobStore.....conditionally update Trigger and JobDetail (if present)
				
				// note: don't overwrite triggers that are currently paused
				if( this.overwriteExistingJobs && !(trigger instanceof ImmutableInitTrigger) && existingTriggerStatus != Trigger.STATE_PAUSED) {
					if( jobDetail != null ) {
						this.addJobDetailToScheduler( jobDetail );
					}
					// update trigger in database
					this.scheduler.rescheduleJob( trigger.getName(), trigger.getGroup(), trigger );
				}
			}
		}
	}
	
	/** 
	 * Schedule all registered job details to a scheduler
	 * @param triggers A list of triggers to schedule
	 */
	protected void scheduleJobDetails( JobDetail[] jobDetails ) throws SchedulerException {
		for( int i=0; i<triggers.length; i++ ) {
			addJobDetailToScheduler( jobDetails[i] );
		}
	}
	
	/** Add a job to a scheduler. Overwrite a job if needed
	 * @param job A job to add to a scheduler
	 * @throws SchedulerException Thrown in failed to add a job
	 */
	private void addJobDetailToScheduler( JobDetail job ) throws SchedulerException {
		
		// Found an existing job
		if( this.scheduler.getJobDetail( job.getName(), job.getGroup() ) != null ) {
			if( this.overwriteExistingJobs && !(job instanceof ClusterAwareJobDetailBean) ) {
				this.scheduler.addJob( job, this.overwriteExistingJobs );
			}
		}
		else {
			try {
				// To add a job without a trigger, a job must be durable to
				// preserve the existing job.  Otherwise, a job must be overwritten
				boolean overwrite = !job.isDurable() ? true : job.isDurable();
				this.scheduler.addJob( job, overwrite );
			}
			catch( SchedulerException e ) {
				if( logger.isDebugEnabled() ) {
					logger.debug( "Unexpectedly found existing job, assumably due to cluster race condition", e );
				}
				// This means that a race condition might 
				// have happened in a cluster environment
				if( this.overwriteExistingJobs && !(job instanceof ClusterAwareJobDetailBean) ) {
					// Don't care whether a job is durable or not, overwrite it anyway
					this.scheduler.addJob( job, true );
				}
			}
		}
	}

	/**
	 * @return Returns the transactionManager.
	 */
	public PlatformTransactionManager getTransactionManager() {
		return transactionManager;
	}

	/**
	 * @param transactionManager The transactionManager to set.
	 */
	public void setTransactionManager(PlatformTransactionManager transactionManager) {
		this.transactionManager = transactionManager;
	}
}