org.ebayopensource.winder.quartz.QuartzSchedulerManager.java Source code

Java tutorial

Introduction

Here is the source code for org.ebayopensource.winder.quartz.QuartzSchedulerManager.java

Source

/**
 * Copyright (c) 2016 eBay Software Foundation. All rights reserved.
 *
 * Licensed under the MIT license.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 *
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.ebayopensource.winder.quartz;

import org.apache.commons.lang3.StringUtils;
import org.ebayopensource.common.config.InjectProperty;
import org.ebayopensource.winder.*;
import org.quartz.*;
import org.quartz.impl.DefaultThreadExecutor;
import org.quartz.impl.DirectSchedulerFactory;
import org.quartz.impl.JobDetailImpl;
import org.quartz.impl.jdbcjobstore.Constants;
import org.quartz.impl.jdbcjobstore.JobStoreTX;
import org.quartz.impl.matchers.GroupMatcher;
import org.quartz.plugins.history.LoggingJobHistoryPlugin;
import org.quartz.plugins.history.LoggingTriggerHistoryPlugin;
import org.quartz.simpl.CascadingClassLoadHelper;
import org.quartz.simpl.SimpleInstanceIdGenerator;
import org.quartz.simpl.SimpleThreadPool;
import org.quartz.spi.SchedulerPlugin;
import org.quartz.spi.ThreadPool;
import org.quartz.utils.DBConnectionManager;
import org.quartz.utils.PoolingConnectionProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.sql.*;
import java.util.*;
import java.util.Date;
import java.util.concurrent.TimeUnit;

import static org.ebayopensource.winder.StatusEnum.CANCELLED;
import static org.ebayopensource.winder.StatusEnum.PAUSED;
import static org.ebayopensource.winder.StatusEnum.SUBMITTED;
import static org.ebayopensource.winder.quartz.QuartzWinderConstants.*;
import static org.quartz.impl.jdbcjobstore.Constants.COL_JOB_DATAMAP;

/**
 * Schedule Manager
 *
 * @author Sheldon Shao xshao@ebay.com on 10/16/16.
 * @version 1.0
 */
public class QuartzSchedulerManager<TI extends TaskInput> implements WinderSchedulerManager<TI> {

    private Scheduler quartzScheduler = null;

    private WinderEngine engine;

    @InjectProperty(name = "winder.scheduler.step_interval")
    private int defaultStepInterval = 10; //Seconds

    @InjectProperty(name = "winder.scheduler.max_job_duration")
    private int defaultMaxJobDuration = (int) TimeUnit.DAYS.toMillis(7); //Seconds

    private static Logger log = LoggerFactory.getLogger(QuartzSchedulerManager.class);

    private WinderJobDetailFactory jobDetailFactory;

    private boolean inMemoryScheduler = false;

    public QuartzSchedulerManager(WinderEngine engine) {
        this.engine = engine;
        this.jobDetailFactory = engine.getJobDetailFactory();
        init();
    }

    private String dataSourceName;

    private void init() {
        WinderConfiguration configuration = engine.getConfiguration();
        DirectSchedulerFactory factory = DirectSchedulerFactory.getInstance();
        int numThreads = configuration.getInt("winder.quartz.numThreads", 50);

        String quartzType = configuration.getString("winder.quartz.scheduler_type");
        dataSourceName = configuration.getString("winder.quartz.datasource");

        Scheduler scheduler = null;
        boolean inMemoryScheduler = false;
        try {
            if ("IN_MEMORY_SCHEDULER".equals(quartzType) || (quartzType == null && dataSourceName == null)) {
                factory.createVolatileScheduler(numThreads);
                scheduler = factory.getScheduler();

                inMemoryScheduler = true;
                if (log.isInfoEnabled()) {
                    log.info("Scheduler manager starting IN_MEMORY_SCHEDULER");
                }
            } else {
                ThreadPool threadPool = new SimpleThreadPool(numThreads, Thread.NORM_PRIORITY);
                threadPool.initialize();
                String instanceId = (new SimpleInstanceIdGenerator()).generateInstanceId();

                DBConnectionManager dbMgr = DBConnectionManager.getInstance();

                int poolSize = configuration.getInt("winder.quartz.ds.pool_size", numThreads + 15);

                String jdbcUrl = dataSourceName;
                if ("ds".equals(dataSourceName)) {
                    //
                    String jdbcDriver = configuration.getString("winder.quartz.ds.driver");
                    jdbcUrl = configuration.getString("winder.quartz.ds.url");
                    String jdbcUser = configuration.getString("winder.quartz.ds.username");
                    String jdbcPassword = configuration.getString("winder.quartz.ds.password");

                    String validate = configuration.getString("winder.quartz.ds.validate_sql",
                            "SELECT 1 /* ping */");
                    PoolingConnectionProvider pooling = new PoolingConnectionProvider(jdbcDriver, jdbcUrl, jdbcUser,
                            jdbcPassword, poolSize, validate);

                    dbMgr.addConnectionProvider(dataSourceName, pooling);
                } else {
                    log.warn("Please make sure the data source:" + dataSourceName
                            + " has already been initialized in somewhere else");
                }

                boolean enableQuartz = configuration.getBoolean("winder.quartz.enable", true);

                if (enableQuartz) {
                    String tablePrefix = configuration.getString("winder.quartz.ds.table_prefix", "WINDER_");

                    reformat(SELECT_JOBS_LIMIT, tablePrefix);
                    reformat(SELECT_JOBS_LIMIT_BY_DATE_RANGE, tablePrefix);
                    reformat(SELECT_JOBS_LIMIT_LIKE, tablePrefix);
                    reformat(SELECT_JOBS_LIMIT_LIKE_BY_DATE_RANGE, tablePrefix);

                    int checkInterval = configuration.getInt("winder.quartz.checkin_interval", 7500);
                    String clusterName = engine.getClusterName();
                    JobStoreTX jdbcJobStore = new WinderJobStoreTx();
                    jdbcJobStore.setDataSource(dataSourceName);
                    jdbcJobStore.setTablePrefix(tablePrefix);
                    jdbcJobStore.setIsClustered(true);
                    jdbcJobStore.setClusterCheckinInterval(checkInterval);

                    String hostName;
                    try {
                        InetAddress inet = InetAddress.getLocalHost();
                        hostName = inet.getHostName();
                    } catch (UnknownHostException e) {
                        hostName = "unknownHost";
                    }
                    jdbcJobStore.setInstanceId(hostName);
                    jdbcJobStore.setDriverDelegateClass("org.ebayopensource.winder.quartz.WinderJDBCDelegate");
                    jdbcJobStore.setThreadPoolSize(poolSize);

                    // To fix the quartz misfire issue
                    DefaultThreadExecutor executor = new DefaultThreadExecutor();
                    long idleWaitTime = configuration.getLong("winder.quartz.idle_wait_time", 30000L);
                    long dbFailureRetryInterval = configuration.getLong("winder.quartz.db_failure_retry_interval",
                            10000L);
                    long batchTimeWindow = configuration.getLong("winder.quartz.batch_time_window", 1000L);

                    boolean enableQuartzPlugins = configuration.getBoolean("winder.quartz.plugins.enable", false);
                    if (enableQuartzPlugins) {
                        Map<String, SchedulerPlugin> schedulerPluginMap = new HashMap<String, SchedulerPlugin>();
                        schedulerPluginMap.put("LoggingTriggerHistoryPlugin", new LoggingTriggerHistoryPlugin());
                        schedulerPluginMap.put("LoggingJobHistoryPlugin", new LoggingJobHistoryPlugin());

                        factory.createScheduler(clusterName, instanceId, threadPool, executor, jdbcJobStore,
                                schedulerPluginMap, null, 0, idleWaitTime, dbFailureRetryInterval, false, null,
                                numThreads, batchTimeWindow);
                    } else {
                        factory.createScheduler(clusterName, instanceId, threadPool, executor, jdbcJobStore, null,
                                null, 0, idleWaitTime, dbFailureRetryInterval, false, null, numThreads,
                                batchTimeWindow);
                    }
                    scheduler = factory.getScheduler(clusterName);
                    if (log.isInfoEnabled()) {
                        log.info("Scheduler manager starting with:" + jdbcUrl);
                    }
                } else {
                    if (log.isInfoEnabled()) {
                        log.info("Scheduler manager disabled!");
                    }
                }
            }

            this.quartzScheduler = scheduler;
            this.inMemoryScheduler = inMemoryScheduler;
        } catch (Exception e) {
            if (log.isErrorEnabled()) {
                log.error("Failure initializing quartz", e);
            }
            throw new IllegalStateException("Unable to initialize quartz", e);
        }
    }

    public int getDefaultStepInterval() {
        return defaultStepInterval;
    }

    public void setDefaultStepInterval(int defaultStepInterval) {
        this.defaultStepInterval = defaultStepInterval;
    }

    public long getDefaultMaxJobDuration() {
        return defaultMaxJobDuration;
    }

    public void setDefaultMaxJobDuration(int defaultMaxJobDuration) {
        this.defaultMaxJobDuration = defaultMaxJobDuration;
    }

    @Override
    public WinderJobDetail getJobDetail(JobId jobId) throws WinderScheduleException {
        JobKey key = getKey(jobId);
        try {
            JobDetail qjd = quartzScheduler.getJobDetail(key);

            if (qjd instanceof WinderJobDetail) {
                return (WinderJobDetail) qjd;
            } else {
                return new QuartzJobDetail(engine, jobId, qjd);
            }
        } catch (SchedulerException e) {
            throw new WinderScheduleException("Retrieving job detail error", e);
        }
    }

    @Override
    public WinderJobDetail getJobDetail(String jobId) throws WinderScheduleException {
        return getJobDetail(WinderUtil.toJobId(jobId));
    }

    protected JobKey getKey(JobId jobId) {
        if (jobId instanceof QuartzJobId) {
            return ((QuartzJobId) jobId).getKey();
        } else {
            return new JobKey(jobId.getName(), jobId.getGroup());
        }
    }

    @Override
    public void unscheduleJob(JobId jobId) throws WinderScheduleException {
        List<? extends Trigger> triggers = null;

        try {
            triggers = quartzScheduler.getTriggersOfJob(getKey(jobId));
        } catch (SchedulerException se) {
            throw new WinderScheduleException("Querying triggers exception", se);
        }
        if (triggers == null) {
            return;
        }
        for (Trigger trigger : triggers) {
            try {
                quartzScheduler.unscheduleJob(trigger.getKey());
            } catch (SchedulerException e) {
                throw new WinderScheduleException("Unscheduleing job exception", e);
            }
        }
    }

    @Override
    public void updateJobData(WinderJobDetail job) throws WinderScheduleException {
        try {
            quartzScheduler.addJob((JobDetail) job, true);
        } catch (SchedulerException e) {
            throw new WinderScheduleException("Change job data exception", e);
        }
    }

    @Override
    public JobId scheduleChildJob(TI input, WinderJobContext parentJobCtx) throws WinderScheduleException {

        Class clazz = input.getJobClass();
        if (clazz == null) {
            throw new IllegalArgumentException("Need job class ");
        }
        String owner = parentJobCtx.getJobSummary().getOwner();
        if (input.getJobOwner() == null) {
            input.setJobOwner(owner);
        }

        WinderJobDetail jd = jobDetailFactory.createJobDetail(input);

        if (jd instanceof QuartzJobDetail) {
            ((QuartzJobDetail) jd).setParentJobId(parentJobCtx.getJobId());
        }

        JobId jobId = jd.getJobId();

        // Create trigger
        Trigger t = createStagedTrigger(input.getStepInterval(), input.getJobDuration(), input.getJobScheduleTime(),
                jobId);

        // Child job was scheduled, add child job id to list in parent context
        parentJobCtx.getJobDetail().addChildJobIds(jd.getJobId());

        // schedule job
        try {
            quartzScheduler.scheduleJob((JobDetail) jd, t);
            updateJobDetail(parentJobCtx.getJobDetail());
        } catch (SchedulerException e) {
            throw new WinderScheduleException("Error scheduling job", e);
        }

        return jobId;
    }

    @Override
    public boolean doneYet(List<WinderJobDetail> childJobDetails) {
        if (childJobDetails == null) {
            throw new IllegalArgumentException("Details array cannot be null");
        }
        for (WinderJobDetail detail : childJobDetails) {
            if (detail == null) {
                throw new IllegalStateException("Details cannot be null");
            }
            switch (detail.getStatus()) {
            case EXECUTING:
            case CANCEL_IN_PROGRESS:
            case PAUSED: // assumption that CANCEL if really done
            case SUBMITTED:
                return false; // these mean the job isn't done
            default:
                continue; // keep checking
            }
        }
        return true;
    }

    @Override
    public JobId scheduleJob(TI input) throws WinderScheduleException {
        WinderJobDetail jd = jobDetailFactory.createJobDetail(input);

        Date jobStartTime = input.getJobScheduleTime();
        Trigger t = createStagedTrigger(input.getStepInterval(), input.getJobDuration(), jobStartTime,
                jd.getJobId());
        try {
            quartzScheduler.scheduleJob((JobDetail) jd, t);
        } catch (SchedulerException e) {
            throw new WinderScheduleException("Scheduling simple job exception", e);
        }
        return jd.getJobId();
    }

    protected String triggerName(JobId jobId) {
        return TRIGGER_NAME_PREFIX + jobId.getName();
    }

    protected String cronJobTriggerName(JobId jobId) {
        return TRIGGER_NAME_PREFIX + jobId.toString();
    }

    protected Trigger createStagedTrigger(int stageIntervalSec, int maxTimeForJobSec, Date startTime, JobId jobId) {
        return TriggerBuilder.newTrigger().withIdentity(triggerName(jobId), jobId.getGroup())
                .forJob(jobId.getName(), jobId.getGroup()).startAt(startTime)
                .endAt(new Date(startTime.getTime() + maxTimeForJobSec * 1000)).withSchedule(SimpleScheduleBuilder
                        .simpleSchedule().withIntervalInSeconds(stageIntervalSec).repeatForever())
                .build();
    }

    @Override
    public void rescheduleJob(JobId jobId, Date jobStartTime, int stageIntervalSec, int maxTimeForJobSec)
            throws WinderScheduleException {
        if (jobStartTime == null) {
            jobStartTime = new Date();
        }
        String triggerName = triggerName(jobId);
        String triggerGroup = jobId.getGroup();

        Trigger t = createStagedTrigger(stageIntervalSec, maxTimeForJobSec, jobStartTime, jobId);
        rescheduleJob(triggerName, triggerGroup, jobId, jobStartTime, t, true, null);
    }

    @Override
    public void rescheduleCronJob(JobId jobId, Date jobStartTime, String cronExpression)
            throws WinderScheduleException {
        if (jobStartTime == null) {
            jobStartTime = new Date();
        }

        String triggerName = cronJobTriggerName(jobId);
        String triggerGroup = TRIGGER_GROUP_CRON;

        Trigger t = TriggerBuilder.newTrigger().withIdentity(triggerName, triggerGroup)
                .forJob(jobId.getName(), jobId.getGroup()).startAt(jobStartTime)
                .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)).build();

        rescheduleJob(triggerName, triggerGroup, jobId, jobStartTime, t, true, null);
    }

    private void rescheduleJob(String triggerName, String triggerGroup, JobId jobId, Date jobStartTime, Trigger t,
            boolean hasTrigger, WinderJobDetail jobDetails)

            throws WinderScheduleException {

        JobDetail jd = null;
        if (jobDetails == null) {
            jobDetails = getJobDetail(jobId);
        }

        jd = (JobDetail) jobDetails;

        JobDataMap jobMap = jd.getJobDataMap();
        jobMap.put(KEY_JOB_START_DATE, jobStartTime.getTime());

        // reset the status
        changeJobStatus(jobDetails, SUBMITTED);

        try {
            if (hasTrigger) {
                quartzScheduler.rescheduleJob(new TriggerKey(triggerName, triggerGroup), t);
            } else {
                quartzScheduler.scheduleJob(t);
            }
        } catch (SchedulerException se) {
            throw new WinderScheduleException("Scheduling job " + jobId + " exception", se);
        }
    }

    @Override
    public JobId scheduleCronJob(TI input, String cronExpression) throws WinderScheduleException {
        WinderJobDetail jd = jobDetailFactory.createJobDetail(input);

        JobId jobId = jd.getJobId();
        Date startTime = input.getJobScheduleTime();
        Trigger t = TriggerBuilder.newTrigger().withIdentity(cronJobTriggerName(jobId), TRIGGER_GROUP_CRON)
                .forJob(jobId.getName(), jobId.getGroup()).startAt(startTime)
                .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)).build();

        try {
            quartzScheduler.scheduleJob((JobDetail) jd, t);
        } catch (SchedulerException e) {
            throw new WinderScheduleException("Scheduling cron job exception", e);
        }
        return jobId;
    }

    @Override
    public void pauseJob(JobId jobId) throws WinderScheduleException {
        WinderJobDetail d = checkJobStatusChange(jobId, PAUSED, new StatusEnum[] { StatusEnum.SUBMITTED,
                StatusEnum.PAUSED, StatusEnum.ERROR, StatusEnum.EXECUTING });
        try {
            quartzScheduler.pauseJob(getKey(jobId));
        } catch (SchedulerException e) {
            throw new WinderScheduleException("Pausing job exception", e);
        }
        changeJobStatus(d, StatusEnum.PAUSED);
    }

    @Override
    public void resumeJob(JobId jobId, StatusEnum newStatus, String message, Boolean autoPause, String user)
            throws WinderScheduleException {

        // Cases                                                message
        // 1. regular Resume                                 ""
        // 2. Resume with awaitingforAction                        ""
        // 3. Cancel(by owner) with awaitingForAction(during paused)      ""
        // 4. Cancel(by LOM) with awaitingForAction(during paused)      "someMessage"
        // 5. Regular Cancel(during paused)                        ""

        WinderJobDetail jobDetail = checkJobStatusChange(jobId, newStatus, new StatusEnum[] { PAUSED });
        String jobStatusMessage;
        String actionMsg = (newStatus.equals(StatusEnum.CANCEL_IN_PROGRESS)) ? "cancelled" : "resumed";
        jobStatusMessage = "Job " + actionMsg + " by: " + user + ". ";

        if (jobDetail.isAwaitingForAction()
                || (newStatus.equals(StatusEnum.CANCEL_IN_PROGRESS) && !StringUtils.isEmpty(message))) {
            UserActionType action = (newStatus.equals(StatusEnum.CANCEL_IN_PROGRESS)) ? UserActionType.CANCELLED
                    : UserActionType.RESUMED;
            message = StringUtils.isEmpty(message) ? jobStatusMessage : message;
            markAlert(jobId, jobDetail, action, message, user);

            jobDetail.setAwaitingForAction(false);
            updateJobData(jobDetail);
        }
        try {
            quartzScheduler.resumeJob(getKey(jobId));
        } catch (SchedulerException e) {
            throw new WinderScheduleException("Resuming job error", e);
        }

        if (StringUtils.isNotEmpty(jobStatusMessage)) {
            changeJobStatus(jobDetail, newStatus, jobStatusMessage, autoPause);
        } else {
            changeJobStatus(jobDetail, newStatus);
        }
    }

    public void markAlert(JobId jobId, WinderJobDetail jobDetail, UserActionType action, String message,
            String user) throws WinderScheduleException {

        if (jobDetail == null) {
            jobDetail = getJobDetail(jobId);
        }
        boolean awaitingForAction = action.equals(UserActionType.PAUSED);
        jobDetail.setAwaitingForAction(awaitingForAction);

        if (StringUtils.isEmpty(message)) {
            message = "Job " + action.toString().toLowerCase() + " by " + user + " .";
        }

        jobDetail.addUserAction(action, message, user);

        if (log.isDebugEnabled()) {
            log.debug("The alert is being updated in job details. JobId : " + jobId + ", action : " + action
                    + ", awaitingForAction : " + awaitingForAction);
        }
        updateJobDetail(jobDetail);
    }

    protected void updateJobDetail(WinderJobDetail jobDetail) throws WinderScheduleException {
        try {
            quartzScheduler.addJob((JobDetail) jobDetail, true);
        } catch (SchedulerException e) {
            throw new WinderScheduleException("Change job data exception", e);
        }
    }

    @Override
    public void cancelJob(JobId jobId, boolean force) throws WinderScheduleException {
        WinderJobDetail jobDetail = checkJobStatusChange(jobId, CANCELLED,
                force ? null
                        : new StatusEnum[] { StatusEnum.SUBMITTED, CANCELLED, StatusEnum.CANCEL_IN_PROGRESS, PAUSED,
                                StatusEnum.ERROR, StatusEnum.UNKNOWN, StatusEnum.EXECUTING });
        unscheduleJob(jobId);
        changeJobStatus(jobDetail, CANCELLED);
    }

    @Override
    public void markCancelInProgress(JobId jobId, String message, String user) throws WinderScheduleException {
        //This will work, check Job for race condition resolution
        WinderJobDetail jobDetail = checkJobStatusChange(jobId, StatusEnum.CANCEL_IN_PROGRESS,
                new StatusEnum[] { StatusEnum.SUBMITTED, StatusEnum.PAUSED, StatusEnum.ERROR, StatusEnum.EXECUTING,
                        StatusEnum.CANCEL_IN_PROGRESS });

        if (StringUtils.isNotEmpty(message)) {
            changeJobStatus(jobDetail, StatusEnum.CANCEL_IN_PROGRESS, "Job Cancelled by: " + user + ".", null);
            markAlert(jobId, jobDetail, UserActionType.CANCELLED, message, user);
        } else {
            changeJobStatus(jobDetail, StatusEnum.CANCEL_IN_PROGRESS);
        }
    }

    @Override
    public boolean successChildJob(List<WinderJobDetail> childJobDetails) {
        if (childJobDetails == null) {
            throw new IllegalArgumentException("Details array cannot be null");
        }
        for (int i = 0; i < childJobDetails.size(); i++) {
            WinderJobDetail detail = childJobDetails.get(i);
            if (detail == null) {
                throw new IllegalStateException("Details cannot be null");
            }
            switch (detail.getStatus()) {
            case COMPLETED:
            case CANCELLED:
                return true;
            default:
                return false;
            }
        }
        return true;
    }

    @Override
    public List<WinderJobDetail> listJobDetails(JobFilter filter) throws WinderScheduleException {
        if (!inMemoryScheduler) {
            return selectJobs(filter);
        } else {
            return fetchInMemory(filter);
        }
    }

    public List<WinderJobDetail> fetchInMemory(JobFilter filter) throws WinderScheduleException {

        List<WinderJobDetail> jobs = new ArrayList<>();
        // start making queries
        boolean hasDateRange = false;

        Date start = filter.getStart();
        long startTime = 0, endTime = 0;
        if (start != null) {
            hasDateRange = true;

            startTime = start.getTime();

            endTime = (filter.getEnd() == null) ? System.currentTimeMillis() : filter.getEnd().getTime();
        }

        try {
            List<String> groupNames = quartzScheduler.getJobGroupNames();
            for (String group : groupNames) {
                JobKeyField keyField = filter.getKeyField();

                boolean matched = false;
                if (keyField == JobKeyField.JOB_GROUP) {
                    if (filter.isLike()) {
                        matched = group.contains(filter.getValue());
                    } else {
                        matched = group.equals(filter.getValue());
                    }
                } else if (keyField == JobKeyField.ALL) {
                    matched = true;
                }

                GroupMatcher<JobKey> groupMatcher = GroupMatcher.groupEquals(group);
                Set<JobKey> keys = quartzScheduler.getJobKeys(groupMatcher);

                for (JobKey key : keys) {
                    if (!matched) { //Job NAME
                        String jobName = key.getName();
                        if (filter.isLike()) {
                            matched = jobName.contains(filter.getValue());
                        } else {
                            matched = jobName.equals(filter.getValue());
                        }
                    }
                    if (matched) {
                        WinderJobDetail jobDetail = getJobDetail(new QuartzJobId(key, engine.getClusterName()));

                        if (hasDateRange) {
                            long date = jobDetail.getCreated().getTime();
                            if (date >= startTime && date < endTime) {
                                jobs.add(jobDetail);
                            }
                        } else {
                            jobs.add(jobDetail);
                        }
                    }
                }
            }
        } catch (Exception ex) {
            log.error("Error fetching groups & jobs ", ex);
            throw new WinderScheduleException("Error fetching groups & jobs ", ex);
        }
        return limit(jobs, filter);
    }

    private static void reformat(String[] array, String prefix) {
        for (int i = 0; i < array.length; i++) {
            array[i] = array[i].replace("{0}", prefix);
        }
    }

    private final String[] SELECT_JOBS_LIMIT = new String[] {
            "SELECT * FROM {0}JOB_DETAILS ORDER BY JOB_CREATED DESC LIMIT ?, ?",
            "SELECT * FROM {0}JOB_DETAILS WHERE JOB_NAME = ? ORDER BY JOB_CREATED DESC LIMIT ?, ?",
            "SELECT * FROM {0}JOB_DETAILS WHERE JOB_GROUP = ? ORDER BY JOB_CREATED DESC LIMIT ?, ?" };

    private final String[] SELECT_JOBS_LIMIT_BY_DATE_RANGE = new String[] {
            "SELECT * FROM {0}JOB_DETAILS WHERE JOB_CREATED >= ? AND JOB_CREATED < ? ORDER BY JOB_CREATED DESC LIMIT ?, ?",
            "SELECT * FROM {0}JOB_DETAILS WHERE JOB_NAME = ? AND JOB_CREATED >= ? AND JOB_CREATED < ? ORDER BY JOB_CREATED DESC LIMIT ?, ?",
            "SELECT * FROM {0}JOB_DETAILS WHERE JOB_GROUP = ? AND JOB_CREATED >= ? AND JOB_CREATED < ? ORDER BY JOB_CREATED DESC LIMIT ?, ?" };

    private final String[] SELECT_JOBS_LIMIT_LIKE = new String[] {
            "SELECT * FROM {0}JOB_DETAILS ORDER BY JOB_CREATED DESC LIMIT ?, ?",
            "SELECT * FROM {0}JOB_DETAILS WHERE JOB_NAME LIKE '%?%' ORDER BY JOB_CREATED DESC LIMIT ?, ?",
            "SELECT * FROM {0}JOB_DETAILS WHERE JOB_GROUP LIKE '%?%' ORDER BY JOB_CREATED DESC LIMIT ?, ?" };

    private final String[] SELECT_JOBS_LIMIT_LIKE_BY_DATE_RANGE = new String[] {
            "SELECT * FROM {0}JOB_DETAILS WHERE JOB_CREATED >= ? AND JOB_CREATED < ? ORDER BY JOB_CREATED DESC LIMIT ?, ?",
            "SELECT * FROM {0}JOB_DETAILS WHERE JOB_CREATED >= ? AND JOB_CREATED < ? AND JOB_NAME LIKE '%?%' ORDER BY JOB_CREATED DESC LIMIT ?, ?",
            "SELECT * FROM {0}JOB_DETAILS WHERE JOB_CREATED >= ? AND JOB_CREATED < ? AND JOB_GROUP LIKE '%?%' ORDER BY JOB_CREATED DESC LIMIT ?, ?" };

    public List<WinderJobDetail> selectJobs(JobFilter filter) throws WinderScheduleException {

        List<WinderJobDetail> jobs = new ArrayList<>();
        Connection connection = null;
        PreparedStatement ps = null;

        CascadingClassLoadHelper cascadingClassLoadHelper = new CascadingClassLoadHelper();
        cascadingClassLoadHelper.initialize();
        ResultSet rs = null;
        try {
            connection = DBConnectionManager.getInstance().getConnection(dataSourceName);

            // start making queries
            boolean hasDateRange = false;

            Date start = filter.getStart();
            Timestamp startTime = null, endTime = null;
            if (start != null) {
                hasDateRange = true;

                startTime = new Timestamp(start.getTime());

                endTime = (filter.getEnd() == null) ? new Timestamp(System.currentTimeMillis())
                        : new Timestamp(filter.getEnd().getTime());
            }

            String[] sqls = null;
            if (filter.isLike()) {
                sqls = hasDateRange ? SELECT_JOBS_LIMIT_LIKE_BY_DATE_RANGE : SELECT_JOBS_LIMIT_LIKE;
            } else {
                sqls = hasDateRange ? SELECT_JOBS_LIMIT_BY_DATE_RANGE : SELECT_JOBS_LIMIT;
            }

            JobKeyField keyField = filter.getKeyField();
            ps = connection.prepareStatement(sqls[keyField.ordinal()]);

            if (hasDateRange) {
                if (keyField == JobKeyField.ALL) {
                    ps.setTimestamp(1, startTime);
                    ps.setTimestamp(2, endTime);
                    ps.setInt(3, filter.getOffset());
                    ps.setInt(4, filter.getLimit());
                } else {
                    if (filter.isLike()) {
                        ps.setTimestamp(1, startTime);
                        ps.setTimestamp(2, endTime);
                        ps.setString(3, filter.getValue());
                        ps.setInt(4, filter.getOffset());
                        ps.setInt(5, filter.getLimit());
                    } else {
                        ps.setString(1, filter.getValue());
                        ps.setTimestamp(2, startTime);
                        ps.setTimestamp(3, endTime);
                        ps.setInt(4, filter.getOffset());
                        ps.setInt(5, filter.getLimit());
                    }
                }
            } else {
                if (keyField == JobKeyField.ALL) {
                    ps.setInt(1, filter.getOffset());
                    ps.setInt(2, filter.getLimit());
                } else {
                    ps.setString(1, filter.getValue());
                    ps.setInt(2, filter.getOffset());
                    ps.setInt(3, filter.getLimit());
                }
            }

            rs = ps.executeQuery();

            while (rs.next()) {
                QuartzJobDetail jobDetail = makeJobDetail(cascadingClassLoadHelper, rs);
                jobs.add(jobDetail);
            }
        } catch (Exception e) {
            throw new WinderScheduleException("Job listing failed", e);
        } finally {
            close(rs);
            close(ps);
            close(connection);
        }
        return jobs;
    }

    private QuartzJobDetail makeJobDetail(CascadingClassLoadHelper cascadingClassLoadHelper, ResultSet rs)
            throws SQLException, ClassNotFoundException, IOException {
        JobDetailImpl jobDetail = new JobDetailImpl();

        String groupName = rs.getString(Constants.COL_JOB_GROUP);
        String jobName = rs.getString(Constants.COL_JOB_NAME);
        jobDetail.setName(jobName);
        jobDetail.setGroup(groupName);
        jobDetail.setDescription(rs.getString(Constants.COL_DESCRIPTION));
        jobDetail.setJobClass(cascadingClassLoadHelper.loadClass(rs.getString(Constants.COL_JOB_CLASS), Job.class));
        jobDetail.setDurability(rs.getBoolean(Constants.COL_IS_DURABLE));
        jobDetail.setRequestsRecovery(rs.getBoolean(Constants.COL_REQUESTS_RECOVERY));

        Map<?, ?> map = (Map<?, ?>) getObjectFromBlob(rs, COL_JOB_DATAMAP);

        if (map != null) {
            jobDetail.setJobDataMap(new JobDataMap(map));
        }

        JobId jobId = new QuartzJobId(groupName, jobName, engine.getClusterName());

        QuartzJobDetail quartzJobDetail = new QuartzJobDetail(engine, jobId, jobDetail,
                rs.getTimestamp(WinderJDBCDelegate.COL_JOB_CREATED));
        return quartzJobDetail;
    }

    private Object getObjectFromBlob(ResultSet rs, String colName)
            throws ClassNotFoundException, IOException, SQLException {
        Object obj = null;

        Blob blobLocator = rs.getBlob(colName);
        if (blobLocator != null && blobLocator.length() != 0) {
            InputStream binaryInput = blobLocator.getBinaryStream();

            if (null != binaryInput) {
                if (binaryInput instanceof ByteArrayInputStream
                        && ((ByteArrayInputStream) binaryInput).available() == 0) {
                    //do nothing
                } else {
                    ObjectInputStream in = new ObjectInputStream(binaryInput);
                    try {
                        obj = in.readObject();
                    } finally {
                        in.close();
                    }
                }
            }

        }
        return obj;
    }

    private static void close(ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                log.warn("Error when closing Result Set", e);
            }
        }
    }

    private static void close(Statement statement) {
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                log.warn("Error when closing PreparedStatement", e);
            }
        }
    }

    private static void close(Connection connection) {
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                log.warn("Error when closing the JDBC connection", e);
            }
        }
    }

    private List<WinderJobDetail> limit(List<WinderJobDetail> jobs, JobFilter filter) {
        int skip = filter.getOffset();
        int limit = filter.getLimit();

        List<WinderJobDetail> result = new ArrayList<>();

        int size = jobs.size();
        skip = skip < 0 ? 0 : skip > size ? size : skip;

        for (int i = 0; i < size; i++) {
            if (i < skip) {
                continue;
            }

            if (limit > 0 && result.size() == limit) {
                break;
            }

            result.add(jobs.get(i));
        }

        return result;
    }

    private void changeJobStatus(WinderJobDetail d, StatusEnum newStatus) throws WinderScheduleException {
        changeJobStatus(d, newStatus, null, null);
    }

    private WinderJobDetail checkJobStatusChange(JobId jobId, StatusEnum newStatus, StatusEnum[] oldStatuses)
            throws WinderScheduleException {

        WinderJobDetail jd = getJobDetail(jobId);
        if (jd == null) {
            throw new IllegalArgumentException("job does not exist for id=" + jobId);
        }

        // If old status was not unknown, validate
        if (oldStatuses != null) {
            StatusEnum currentStatus = jd.getStatus();
            boolean match = false;
            StringBuilder possibles = new StringBuilder();
            for (StatusEnum e : oldStatuses) {
                if (e == currentStatus) {
                    match = true;
                    break;
                } else {
                    possibles.append(' ').append(e.name());
                }
            }
            if (!match) {
                String msg;
                if (oldStatuses.length == 1) {
                    msg = "Unexpected status.  Expected" + possibles + " but found " + currentStatus + " for "
                            + jobId;
                } else {
                    msg = "Unexpected status.  Expected one of (" + possibles + " ) but found " + currentStatus
                            + " for " + jobId;
                }
                throw new WinderScheduleException(msg);
            }
        }
        return jd;
    }

    // NOTE: this must be called after checkJobStatusChange is done!!
    private void changeJobStatus(WinderJobDetail d, StatusEnum newStatus, String jobStatusMessage,
            Boolean autoPause) throws WinderScheduleException {
        if (autoPause != null) {
            d.setAutoPause(autoPause);
        }

        d.setStatus(newStatus);

        JobDetail qjd = (JobDetail) d;
        qjd.getJobDataMap().put(KEY_IS_REPLACE_JOB, "y");

        if (StringUtils.isNotBlank(jobStatusMessage)) {
            d.addUpdate(newStatus, jobStatusMessage);
        }

        if ((CANCELLED == newStatus) || (StatusEnum.ERROR == newStatus)) {
            // set end date if the job is canceled or errored
            d.setEndTime(new Date());
        }
        updateJobDetail(d);
    }

    public void start() {
        if (quartzScheduler != null) {
            try {
                quartzScheduler.start();
            } catch (SchedulerException e) {
                log.warn("Starting quartz exception", e);
            }
        }
    }

    public void stop() {
        if (quartzScheduler != null) {
            try {
                quartzScheduler.shutdown(true);
            } catch (SchedulerException e) {
                log.warn("Stopping quartz exception", e);
            }
        }
    }
}