Quartz Scheduler

Quartz Scheduler

Overview

In this tutorial, I will give you the steps how to implement Quartz Scheduler. Some prerequisites is beyond the scope of this tutorial.

Tutorial Overview

This tutorial offers a comprehensive guide to integrating and utilizing Quartz Scheduler within a Spring Boot application. Quartz is a powerful, open-source job scheduling library that can be integrated into virtually any Java application. I will teach you how to create a new scheduler job, as well as how to modify one, and the different types of jobs—from a simple job to a chronological one. Finally, I will show you how you can shut down the Quartz Scheduler as well as restart it.

Prerequisites

Before you begin, ensure you have the following installed:

JDK 21 or later
Maven 3.9.6 or later
Docker
Additionally, familiarity with Spring Boot and PostgreSQL is recommended to fully understand the project setup and functionality.

TMDB (The Movie Database) API Generator

Fort this tutorial you need to create a TMDB API Key.

  • Go to the following url https://www.themoviedb.org/
  • If you don’t have an account create one.
  • After account has been created or you have logged in go to the Settings.
  • Select API
  • In the Tab select Generate or Regenerate Key
  • Copy the API Read Access Token
  • Paste the Access Token in the application.yaml file under
fusion-plex:
  bearer-token:

Project Setup

To setup the project, you need to checkout the demo project and run the following commands.

git clone -b start git@github.com:softwarebuilding-io/quartz-scheduler.git

After cloning the application, run the following command to create the tables.

mvn clean compile -P docker

Import your application into your favourite IDE and then run the application.

Start Application

After the application starts, please check the following URL: http://localhost:8080/fusion-plex/

Step 1: Update the POM

We will start by updating the pom.xml file to add all the required dependencies.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.10.0</version>
</dependency>

Step 2: Updating and Creating SQL Script

After updating the pom, we will update the create-tables.sql script, which will create one new table:

CREATE TABLE public.scheduler_job_info
(
   id UUID NOT NULL,
   job_name VARCHAR(80) NOT NULL UNIQUE,
   job_group VARCHAR(80) NOT NULL,
   description VARCHAR(120) NOT NULL,
   cron_expression VARCHAR(40) NULL,
   job_class VARCHAR(80) NOT NULL,
   job_status SMALLINT NOT NULL,
   cron_job SMALLINT NOT NULL,
   start_time TIME NULL,
   repeat_interval BIGINT NULL,
   repeat_count INTEGER NULL,
   PRIMARY KEY (id)
);

Next, we will create a new script called create-quartz-tables.sql:

BEGIN;

CREATE TABLE qrtz_job_details
(
    sched_name        VARCHAR(120) NOT NULL,
    job_name          VARCHAR(200) NOT NULL,
    job_group         VARCHAR(200) NOT NULL,
    description       VARCHAR(250) NULL,
    job_class_name    VARCHAR(250) NOT NULL,
    is_durable        BOOL         NOT NULL,
    is_nonconcurrent  BOOL         NOT NULL,
    is_update_data    BOOL         NOT NULL,
    requests_recovery BOOL         NOT NULL,
    job_data          BYTEA NULL,
    PRIMARY KEY (sched_name, job_name, job_group)
);

CREATE TABLE qrtz_triggers
(
    sched_name     VARCHAR(120) NOT NULL,
    trigger_name   VARCHAR(200) NOT NULL,
    trigger_group  VARCHAR(200) NOT NULL,
    job_name       VARCHAR(200) NOT NULL,
    job_group      VARCHAR(200) NOT NULL,
    description    VARCHAR(250) NULL,
    next_fire_time BIGINT NULL,
    prev_fire_time BIGINT NULL,
    priority       INTEGER NULL,
    trigger_state  VARCHAR(16)  NOT NULL,
    trigger_type   VARCHAR(8)   NOT NULL,
    start_time     BIGINT       NOT NULL,
    end_time       BIGINT NULL,
    calendar_name  VARCHAR(200) NULL,
    misfire_instr  SMALLINT NULL,
    job_data       BYTEA NULL,
    PRIMARY KEY (sched_name, trigger_name, trigger_group),
    FOREIGN KEY (sched_name, job_name, job_group) REFERENCES qrtz_job_details (sched_name, job_name, job_group)
);

CREATE TABLE qrtz_simple_triggers
(
    sched_name      VARCHAR(120) NOT NULL,
    trigger_name    VARCHAR(200) NOT NULL,
    trigger_group   VARCHAR(200) NOT NULL,
    repeat_count    BIGINT       NOT NULL,
    repeat_interval BIGINT       NOT NULL,
    times_triggered BIGINT       NOT NULL,
    PRIMARY KEY (sched_name, trigger_name, trigger_group),
    FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group)
);

CREATE TABLE qrtz_cron_triggers
(
    sched_name      VARCHAR(120) NOT NULL,
    trigger_name    VARCHAR(200) NOT NULL,
    trigger_group   VARCHAR(200) NOT NULL,
    cron_expression VARCHAR(120) NOT NULL,
    time_zone_id    VARCHAR(80),
    PRIMARY KEY (sched_name, trigger_name, trigger_group),
    FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group)
);

CREATE TABLE qrtz_simprop_triggers
(
    sched_name    VARCHAR(120) NOT NULL,
    trigger_name  VARCHAR(200) NOT NULL,
    trigger_group VARCHAR(200) NOT NULL,
    str_prop_1    VARCHAR(512) NULL,
    str_prop_2    VARCHAR(512) NULL,
    str_prop_3    VARCHAR(512) NULL,
    int_prop_1    INT NULL,
    int_prop_2    INT NULL,
    long_prop_1   BIGINT NULL,
    long_prop_2   BIGINT NULL,
    dec_prop_1    NUMERIC(13, 4) NULL,
    dec_prop_2    NUMERIC(13, 4) NULL,
    bool_prop_1   BOOL NULL,
    bool_prop_2   BOOL NULL,
    PRIMARY KEY (sched_name, trigger_name, trigger_group),
    FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group)
);

CREATE TABLE qrtz_blob_triggers
(
    sched_name    VARCHAR(120) NOT NULL,
    trigger_name  VARCHAR(200) NOT NULL,
    trigger_group VARCHAR(200) NOT NULL,
    blob_data     BYTEA NULL,
    PRIMARY KEY (sched_name, trigger_name, trigger_group),
    FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group)
);

CREATE TABLE qrtz_calendars
(
    sched_name    VARCHAR(120) NOT NULL,
    calendar_name VARCHAR(200) NOT NULL,
    calendar      BYTEA        NOT NULL,
    PRIMARY KEY (sched_name, calendar_name)
);

CREATE TABLE qrtz_paused_trigger_grps
(
    sched_name    VARCHAR(120) NOT NULL,
    trigger_group VARCHAR(200) NOT NULL,
    PRIMARY KEY (sched_name, trigger_group)
);

CREATE TABLE qrtz_fired_triggers
(
    sched_name        VARCHAR(120) NOT NULL,
    entry_id          VARCHAR(95)  NOT NULL,
    trigger_name      VARCHAR(200) NOT NULL,
    trigger_group     VARCHAR(200) NOT NULL,
    instance_name     VARCHAR(200) NOT NULL,
    fired_time        BIGINT       NOT NULL,
    sched_time        BIGINT       NOT NULL,
    priority          INTEGER      NOT NULL,
    state             VARCHAR(16)  NOT NULL,
    job_name          VARCHAR(200) NULL,
    job_group         VARCHAR(200) NULL,
    is_nonconcurrent  BOOL NULL,
    requests_recovery BOOL NULL,
    PRIMARY KEY (sched_name, entry_id)
);

CREATE TABLE qrtz_scheduler_state
(
    sched_name        VARCHAR(120) NOT NULL,
    instance_name     VARCHAR(200) NOT NULL,
    last_checkin_time BIGINT       NOT NULL,
    checkin_interval  BIGINT       NOT NULL,
    PRIMARY KEY (sched_name, instance_name)
);

CREATE TABLE qrtz_locks
(
    sched_name VARCHAR(120) NOT NULL,
    lock_name  VARCHAR(40)  NOT NULL,
    PRIMARY KEY (sched_name, lock_name)
);

CREATE INDEX idx_qrtz_j_req_recovery ON qrtz_job_details (sched_name, requests_recovery);
CREATE INDEX idx_qrtz_j_grp ON qrtz_job_details (sched_name, job_group);

CREATE INDEX idx_qrtz_t_j ON qrtz_triggers (sched_name, job_name, job_group);
CREATE INDEX idx_qrtz_t_jg ON qrtz_triggers (sched_name, job_group);
CREATE INDEX idx_qrtz_t_c ON qrtz_triggers (sched_name, calendar_name);
CREATE INDEX idx_qrtz_t_g ON qrtz_triggers (sched_name, trigger_group);
CREATE INDEX idx_qrtz_t_state ON qrtz_triggers (sched_name, trigger_state);
CREATE INDEX idx_qrtz_t_n_state ON qrtz_triggers (sched_name, trigger_name, trigger_group, trigger_state);
CREATE INDEX idx_qrtz_t_n_g_state ON qrtz_triggers (sched_name, trigger_group, trigger_state);
CREATE INDEX idx_qrtz_t_next_fire_time ON qrtz_triggers (sched_name, next_fire_time);
CREATE INDEX idx_qrtz_t_nft_st ON qrtz_triggers (sched_name, trigger_state, next_fire_time);
CREATE INDEX idx_qrtz_t_nft_misfire ON qrtz_triggers (sched_name, misfire_instr, next_fire_time);
CREATE INDEX idx_qrtz_t_nft_st_misfire ON qrtz_triggers (sched_name, misfire_instr, next_fire_time, trigger_state);

CREATE INDEX idx_qrtz_t_nft_st_misfire_grp ON qrtz_triggers (sched_name, misfire_instr, next_fire_time, trigger_group, trigger_state);

CREATE INDEX idx_qrtz_ft_trig_inst_name ON qrtz_fired_triggers (sched_name, instance_name);
CREATE INDEX idx_qrtz_ft_inst_job_req_rcvry ON qrtz_fired_triggers (sched_name, instance_name, requests_recovery);
CREATE INDEX idx_qrtz_ft_j_g ON qrtz_fired_triggers (sched_name, job_name, job_group);
CREATE INDEX idx_qrtz_ft_jg ON qrtz_fired_triggers (sched_name, job_group);
CREATE INDEX idx_qrtz_ft_t_g ON qrtz_fired_triggers (sched_name, trigger_name, trigger_group);
CREATE INDEX idx_qrtz_ft_tg ON qrtz_fired_triggers (sched_name, trigger_group);

COMMIT;

END;

After creating the new SQL script, you need to edit the following bootstrap-database.sh script and add the following:

echo "Creating quartz schedulers ..."

psql -d postgres -U admin -f /var/postgres/create-quartz-tables.sql

Step 3: Job Store Types and Configuration

Job Store Types

In Quartz, there are many types of Job Stores, but I will explain two of them that you can commonly use in your application.

RAM Store

This is the simplest and fastest job store. It uses RAM to store all Quartz data such as jobs, triggers, and calendars. The limitation is that if you restart your application, all the scheduling information will be lost.

JDBC Job Store

This job store can persist all your scheduling information even after you restart your application. The limitation is that it is slower than the RAM store, as it requires accessing the database, which slows down the access time.

Configuring the DataSource for Quartz

For this tutorial, we will implement the JDBC Job Store. To use it, you need to update the application.yaml file:

# Spring Quartz
quartz:
  job-store-type: jdbc
  scheduler-name: fusion-plex-quartz
  jdbc:
    initialize-schema: never
  properties:
    org:
      quartz:
        scheduler:
          instanceName: fusion-plex-quartz
          instanceId: AUTO
        threadPool:
          class: org.quartz.simpl.SimpleThreadPool
          threadPriority: 5
          threadCount: 25
        jobStore:
          dataSource: quartzDS
          class: org.quartz.impl.jdbcjobstore.JobStoreTX
          driverDelegateClass: org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
          useProperties: false
          tablePrefix: public.qrtz_
        dataSource:
          quartzDS:
            driver: org.postgresql.Driver
            URL: jdbc:postgresql://localhost:5432/postgres
            user: admin
            password: admin@1234

Step 4: Application Context Provider

The Application Context Provider class is a utility in a Spring application designed to facilitate access to Spring beans from non-Spring-managed classes, such as Quartz Scheduler jobs.

package io.softwarebuilding.fusionplex.component;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextProvider implements ApplicationContextAware {

    private static ApplicationContext context;

    public static ApplicationContext getApplicationContext() {
        return context;
    }

    @Override
    public void setApplicationContext(@NonNull final ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
}

Step 5: Scheduler Jobs Enum

This step involves creating an enum of all the Scheduler Jobs. It’s not just a simple enum; it’s a helper enum. When you save the Scheduler Job, it will also save the Scheduler Job Class. Quartz requires the full class name so that it can execute it when due.

package io.softwarebuilding.fusionplex.enums;

import io.softwarebuilding.fusionplex.error.FusionPlexException;
import io.softwarebuilding.fusionplex.jobs.SampleJob;
import io.softwarebuilding.fusionplex.jobs.UpdateLatestMoviesJob;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.springframework.scheduling.quartz.QuartzJobBean;

import java.util.ArrayList;
import java.util.List;

public enum ScheduledJobs {

    UPDATE_LATEST_MOVIES(1, "UpdateLatestMovies", UpdateLatestMoviesJob.class),
    SAMPLE_JOB(2, "SampleJob", SampleJob .class);

    private final Integer id;

    private final String jobJame;

    private final Class<?> clazz;

    ScheduledJobs(final Integer id, final String jobJame, final Class<?> clazz) {
        this.id = id;
        this.jobJame = jobJame;
        this.clazz = clazz;
    }

    public int getId() {
        return this.id;
    }

    public String getJobJame() {
        return this.jobJame;
    }

    public Class<?> getClazz() {
        return this.clazz;
    }

    public static ScheduledJobs valueOf(final Integer value) {
        for (final ScheduledJobs job : ScheduledJobs.values()) {
            if (job.getId() == value) {
                return job;
            }
        }

        return null;
    }

    public static ScheduledJobs valueOfJobName(final String jobJame ) {

        for ( final ScheduledJobs job : ScheduledJobs.values() ) {
            if ( job.getJobJame().equalsIgnoreCase( jobJame ) ) {
                return job;
            }
        }
        throw new IllegalArgumentException("Unknown job name: " + jobJame);
    }

    public static ScheduledJobs getClassName( final String jobJame ) {
        for ( final ScheduledJobs job : ScheduledJobs.values() ) {
            if ( job.getClazz().getName().equalsIgnoreCase( jobJame ) ) {
                return job;
            }
        }
        throw new IllegalArgumentException("Unknown job name: " + jobJame);
    }

    public static List<String> getJobClassName() {
        List<String> jobClassName = new ArrayList<>();

        for (final ScheduledJobs jobs : ScheduledJobs.values()) {
            jobClassName.add(jobs.getJobJame());
        }

        return jobClassName;
    }

    public static JobDetail getJobDetail(
            final String jobName, final String jobGroup, final String jobDescription,
            final String jobClass, final boolean isDurable ) {
        return JobBuilder.newJob( ScheduledJobs.getClass( jobClass ) ).withIdentity( jobName, jobGroup )
                .withDescription( jobDescription ).storeDurably( isDurable ).build();
    }

    @SuppressWarnings("unchecked")
    public static Class<? extends QuartzJobBean> getClass(final String jobClass) {
        try {
            return (Class<? extends QuartzJobBean>) Class.forName(jobClass);
        } catch (ClassNotFoundException exception) {
            throw new FusionPlexException("Could not find QuartzJobBean class", exception);
        }
    }
}

Step 6: Scheduler Job Info Entity, Job Statuses Enum, and DTO

The next step is to create the Scheduler Job Info entity, which will store all scheduler jobs. It is not advisable to access Quartz tables directly, as this can cause undesirable effects. Instead, we will manage all our scheduler jobs through our custom table. We’ll also define an enum for different scheduler statuses and a DTO to represent the values displayed on the screen.

Scheduler Job Info Entity
package io.softwarebuilding.fusionplex.entity;

import io.softwarebuilding.fusionplex.enums.JobStatus;
import jakarta.persistence.*;
import java.io.Serial;
import java.time.LocalTime;
import java.util.UUID;

@Entity
@Table(name = "scheduler_job_info")
public class SchedulerJobInfo extends BaseEntity {

    @Serial
    private static final long serialVersionUID = 1512929258946367969L;

    @Id
    @Column(name = "id", nullable = false, updatable = false)
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "job_name", length = 80, nullable = false, unique = true)
    private String jobName;

    @Column(name = "job_group", length = 80, nullable = false)
    private String jobGroup;

    @Column(name = "description", length = 120, nullable = false)
    private String description;

    @Column(name = "cron_expression", length = 40)
    private String cronExpression;

    @Column(name = "job_class", length = 80, nullable = false)
    private String jobClass;

    @Basic(optional = false)
    @Column(name = "job_status", nullable = false, columnDefinition = "smallint")
    private JobStatus jobStatus;

    @Basic(optional = false)
    @Column(name = "cron_job", nullable = false, columnDefinition = "smallint")
    private boolean cronJob;

    @Column(name = "start_time")
    private LocalTime startTime;

    @Column(name = "repeat_interval")
    private Long repeatInterval;

    @Column(name = "repeat_count")
    private Integer repeatCount;
    // Getters and setters omitted for brevity
}
Job Status ENUM
package io.softwarebuilding.fusionplex.enums;

public enum JobStatus {
    SCHEDULED(1, "Scheduled"),
    RESCHEDULED(2, "Rescheduled"),
    SCHEDULED_AND_STARTED(3, "Scheduled & Started"),
    STARTED(4, "Started"),
    PAUSED(5, "Paused"),
    RESUMED(6, "Resumed"),
    UNSCHEDULED(7, "Unscheduled"),
    FAILED(8, "Failed");

    private final Integer id;
    private final String description;

    JobStatus(final Integer id, final String description) {
        this.id = id;
        this.description = description;
    }

    public Integer getId() {
        return id;
    }

    public String getDescription() {
        return description;
    }
}
DTO for Scheduler Job Info
package io.softwarebuilding.fusionplex.dto;

import io.softwarebuilding.fusionplex.enums.CronJob;
import io.softwarebuilding.fusionplex.enums.JobStatus;
import io.softwarebuilding.fusionplex.enums.ScheduledJobs;
import io.softwarebuilding.fusionplex.error.FusionPlexException;
import io.softwarebuilding.fusionplex.validator.annotation.ValidCronExpression;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;

import java.io.Serial;
import java.io.Serializable;
import java.time.LocalTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;

public class SchedulerJobInfoDto implements Serializable {

    @Serial
    private static final long serialVersionUID = -4753075867231279773L;

    private UUID id;

    @NotEmpty(message = "Job name is required")
    private String jobName;

    @NotEmpty(message = "Job group is required")
    private String jobGroup;

    @NotEmpty(message = "Job description is required")
    private String description;

    @ValidCronExpression(message = "Invalid cron expression")
    private String cronExpression;

    @NotNull(message = "Please select a job class")
    private ScheduledJobs jobClass;

    private JobStatus jobStatus;

    private CronJob cronJob;

    private LocalTime startTime;

    private Long repeatInterval;

    private Integer repeatCount;

    public UUID getId() {
        return id;
    }

    public void setId(final UUID id) {
        this.id = id;
    }

    public String getJobName() {
        return jobName;
    }

    public void setJobName(final String jobName) {
        this.jobName = jobName;
    }

    public String getJobGroup() {
        return jobGroup;
    }

    public void setJobGroup(final String jobGroup) {
        this.jobGroup = jobGroup;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(final String description) {
        this.description = description;
    }

    public String getCronExpression() {
        return cronExpression;
    }

    public void setCronExpression(final String cronExpression) {
        this.cronExpression = cronExpression;
    }

    public ScheduledJobs getJobClass() {
        return this.jobClass;
    }

    public void setJobClass(final ScheduledJobs jobClass) {
        this.jobClass = jobClass;
    }

    public JobStatus getJobStatus() {
        return jobStatus;
    }

    public void setJobStatus(final JobStatus jobStatus) {
        this.jobStatus = jobStatus;
    }

    public CronJob getCronJob() {
        return this.cronJob;
    }

    public void setCronJob(final CronJob cronJob) {
        this.cronJob = cronJob;
    }

    public LocalTime getStartTime() {
        return startTime;
    }

    public void setStartTime(final LocalTime startTime) {
        this.startTime = startTime;
    }

    public Long getRepeatInterval() {
        return repeatInterval;
    }

    public void setRepeatInterval(final Long repeatInterval) {
        this.repeatInterval = repeatInterval;
    }

    public Integer getRepeatCount() {
        return repeatCount;
    }

    public void setRepeatCount(final Integer repeatCount) {
        this.repeatCount = repeatCount;
    }

    public List<String> getJobClassName() {
        return ScheduledJobs.getJobClassName();
    }
}

Step 7: Scheduler Job Creator

The JobSchedulerCreator class is designed to abstract the complexity of configuring Quartz jobs and triggers, providing utility methods for setting up scheduled tasks within a Spring application.

package io.softwarebuilding.fusionplex.component;

import org.quartz.CronTrigger;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.SimpleTrigger;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean;
import org.springframework.stereotype.Component;

import java.text.ParseException;
import java.util.Date;
import java.util.Map;
import java.util.TimeZone;

@Component
public class JobSchedulerCreator {

    public JobDetail createJob(
            final Class<? extends QuartzJobBean> jobClass,
            final boolean isDurable,
            final ApplicationContext context,
            final String jobName,
            final String jobGroup,
            final Map<String, ?> jobParameters) {

        final JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
        factoryBean.setJobClass(jobClass);
        factoryBean.setName(jobName);
        factoryBean.setDurability(true);
        factoryBean.setGroup(jobGroup);

        final JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.put(jobName + jobGroup, jobClass.getName());
        factoryBean.setJobDataAsMap(jobDataMap);

        if (jobParameters != null) {
            factoryBean.setJobDataAsMap(jobParameters);
        }

        factoryBean.afterPropertiesSet();

        return factoryBean.getObject();
    }

    public CronTrigger createCronTrigger(
            final String triggerName,
            final String triggerGroup,
            final String triggerDescription,
            final Date startTime,
            final String cronExpression,
            final int misFireInstruction ) {
        final CronTriggerFactoryBean factoryBean = new CronTriggerFactoryBean();
        factoryBean.setName( triggerName );
        factoryBean.setGroup( triggerGroup );
        factoryBean.setDescription( triggerDescription );
        factoryBean.setStartTime( startTime );
        factoryBean.setCronExpression( cronExpression );
        factoryBean.setMisfireInstruction( misFireInstruction );
        factoryBean.setTimeZone( TimeZone.getTimeZone( "GMT" ) );

        try {
            factoryBean.afterPropertiesSet();
        } catch ( final ParseException e ) {

        }

        return factoryBean.getObject();
    }

    public SimpleTrigger createSimpleTrigger(
            final String triggerName,
            final String triggerGroup,
            final String triggerDescription,
            final Date startTime,
            final Long repeatInterval,
            final Integer repeatCount,
            final int misFireInstruction ) {
        final SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
        factoryBean.setName( triggerName );
        factoryBean.setGroup( triggerGroup );
        factoryBean.setDescription( triggerDescription );
        factoryBean.setStartTime( startTime );
        factoryBean.setRepeatInterval( 0 );
        factoryBean.setRepeatCount( 0 );

        if ( repeatInterval != null ) {
            factoryBean.setRepeatInterval( repeatInterval );
        }

        if ( repeatCount != null ) {
            factoryBean.setRepeatCount( repeatCount );
        }

        factoryBean.setMisfireInstruction( misFireInstruction );
        factoryBean.afterPropertiesSet();

        return factoryBean.getObject();
    }
}

Step 8: Validators and Converters

When creating a job, if it is a cron job, we need to ensure the cron expression is correct. Therefore, we will include a validator in the DTO. Additionally, to display boolean values and date-related fields on the Thymeleaf page, we need converters that handle conversion from String to Boolean and vice versa, and for handling date fields. We also need a dropdown for selecting whether it is a cron job or not.

Cron Validator

This includes the annotation and its implementation.

package io.softwarebuilding.fusionplex.validator.annotation;

import io.softwarebuilding.fusionplex.validator.CronExpressionValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = CronExpressionValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidCronExpression {

    String message() default "Invalid Cron Expression";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
package io.softwarebuilding.fusionplex.validator;

import io.softwarebuilding.fusionplex.validator.annotation.ValidCronExpression;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.quartz.CronExpression;
import java.text.ParseException;

public class CronExpressionValidator implements ConstraintValidator<ValidCronExpression, String> {

    @Override
    public void initialize(final ValidCronExpression constraintAnnotation) {
    }

    @Override
    public boolean isValid(final String cronExpression, final ConstraintValidatorContext context) {
        if (cronExpression == null || cronExpression.isEmpty()) {
            return true;
        }
        try {
            new CronExpression(cronExpression);
            return true;
        } catch (final ParseException exception) {
            return false;
        }
    }
}
Converters

The following code handles various types of conversions:

package io.softwarebuilding.fusionplex.converter;

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;

@Converter(autoApply = true)
public class BooleanToSmallintConverter implements AttributeConverter<Boolean, Integer> {

    @Override
    public Integer convertToDatabaseColumn(final Boolean attribute) {
        if (attribute == null) {
            return null;
        }

        return attribute ? 1 : 0;
    }

    @Override
    public Boolean convertToEntityAttribute(final Integer dbColumnValue) {

        if (dbColumnValue == null) {
            return null;
        }

        return dbColumnValue != 0;
    }
}
package io.softwarebuilding.fusionplex.converter;

import io.softwarebuilding.fusionplex.util.FusionPlexUtil;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import java.time.LocalTime;

@Component
public class LocalTimeToStringConverter implements Converter<LocalTime, String> {
    @Override
    public String convert(@NotNull final LocalTime localTime) {
        return FusionPlexUtil.convertLocalTimeToString(localTime, "HH:mm");
    }
}
package io.softwarebuilding.fusionplex.converter;

import io.softwarebuilding.fusionplex.enums.ScheduledJobs;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

@Component
public class ScheduledJobsConverter implements Converter<String, ScheduledJobs> {
    @Override
    public ScheduledJobs convert(@NotNull final String value) {
        return ScheduledJobs.valueOfJobName(value);
    }
}
package io.softwarebuilding.fusionplex.converter;

import io.softwarebuilding.fusionplex.util.FusionPlexUtil;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import java.time.LocalTime;

@Component
public class StringToLocalTimeConverter implements Converter<String, LocalTime> {
    @Override
    public LocalTime convert(@NotNull final String value) {
        return value.isEmpty() ? null : FusionPlexUtil.convertStringToLocalTime(value, "HH:mm");
    }
}
FusionPlexUtil Class

This class provides utility methods for conversions between different data types:

public class FusionPlexUtil {
    public static String convertLocalTimeToString(final LocalTime localTime, final String pattern) {
        final DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern);
        return localTime.format(dtf);
    }

    public static LocalTime convertStringToLocalTime(final String value, final String pattern) {
        final DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern);
        return LocalTime.parse(value, dtf);
    }

    public static Date localTimeToDate(final LocalTime startTime) {
        final LocalDate currentDate = LocalDate.now();
        final LocalDateTime currentDateTime = LocalDateTime.of(currentDate, startTime);
        return Date.from(currentDateTime.atZone(ZoneId.systemDefault()).toInstant());
    }
}

Step 9: Repository and Service Layer

First, we create the repository layer for the scheduler job info entity:

package io.softwarebuilding.fusionplex.repository;

import io.softwarebuilding.fusionplex.entity.SchedulerJobInfo;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;

public interface SchedulerJobInfoRepository extends JpaRepository<SchedulerJobInfo, UUID> {
}

Next, we implement the service layer. This layer has various methods such as retrieving all scheduled jobs, creating and editing a job, shutting down and starting the Quartz scheduler (which affects the entire Quartz system, not just a single job), unscheduling a job, pausing and resuming a job, and finally, deleting a job.

package io.softwarebuilding.fusionplex.service;

import io.softwarebuilding.fusionplex.component.JobSchedulerCreator;
import io.softwarebuilding.fusionplex.dto.SchedulerJobInfoDto;
import io.softwarebuilding.fusionplex.entity.SchedulerJobInfo;
import io.softwarebuilding.fusionplex.enums.CronJob;
import io.softwarebuilding.fusionplex.enums.JobStatus;
import io.softwarebuilding.fusionplex.enums.ScheduledJobs;
import io.softwarebuilding.fusionplex.error.FusionPlexException;
import io.softwarebuilding.fusionplex.repository.SchedulerJobInfoRepository;
import io.softwarebuilding.fusionplex.util.FusionPlexUtil;
import org.modelmapper.ModelMapper;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Service;

import java.util.*;

@Service
public class SchedulerJobInfoService {

    private static final Logger LOG = LoggerFactory.getLogger(SchedulerJobInfoService.class);

    final Map<String, Object> jobParameters;

    private final SchedulerJobInfoRepository schedulerJobInfoRepository;

    private final SchedulerFactoryBean schedulerFactoryBean;

    private final ApplicationContext applicationContext;

    private final JobSchedulerCreator schedulerCreator;

    private final Scheduler scheduler;

    @Autowired
    public SchedulerJobInfoService(
            final SchedulerJobInfoRepository schedulerJobInfoRepository,
            final SchedulerFactoryBean schedulerFactoryBean,
            final ApplicationContext applicationContext,
            final JobSchedulerCreator schedulerCreator) {
        this.schedulerJobInfoRepository = schedulerJobInfoRepository;
        this.schedulerFactoryBean = schedulerFactoryBean;
        this.applicationContext = applicationContext;
        this.schedulerCreator = schedulerCreator;
        this.scheduler = schedulerFactoryBean.getScheduler();
        this.jobParameters = new HashMap<>();
    }

    public List<SchedulerJobInfoDto> findAll() {
        return this.schedulerJobInfoRepository.findAll().stream()
                .map(schedulerJobInfo -> this.getModelMapper()
                        .map(schedulerJobInfo, SchedulerJobInfoDto.class)).toList();
    }

    public SchedulerJobInfoDto findById(final UUID id) {
        return this.schedulerJobInfoRepository.findById(id)
                .map(schedulerJobInfo -> {
                    final SchedulerJobInfoDto dto = this.getModelMapper().map(schedulerJobInfo, SchedulerJobInfoDto.class);
                    dto.setCronJob(CronJob.convertToCronJob(String.valueOf(schedulerJobInfo.isCronJob())));
                    dto.setJobClass(ScheduledJobs.getClassName(schedulerJobInfo.getJobClass()));

                    return dto;
                })
                .orElseThrow(() -> new FusionPlexException("Scheduler Job Not Found"));
    }

    public void startScheduler() {
        try {

            if (!this.scheduler.isStarted()) {

                final Scheduler newScheduler = this.schedulerFactoryBean.getScheduler();
                newScheduler.start();
            }

        } catch (final SchedulerException exception) {
            LOG.error(exception.getMessage(), exception);
            throw new FusionPlexException("Error starting scheduler", exception);
        }
    }

    public void shutdownScheduler() {
        try {

            if (this.scheduler.isStarted()) {

                this.scheduler.shutdown();
            }

        } catch (final SchedulerException exception) {
            LOG.error(exception.getMessage(), exception);
            throw new FusionPlexException("Error encountered while attempting to shutdown scheduler" + exception.getMessage());
        }
    }

    public void unscheduleJob(final UUID jobId) {
        final SchedulerJobInfo schedulerJobInfo = this.findEntityById(jobId);

        try {
            schedulerJobInfo.setJobStatus(JobStatus.UNSCHEDULED);

            if (!this.scheduler.isStarted()) {
                this.scheduler.start();
            }

            final TriggerKey triggerKey = this.getTriggerKey(schedulerJobInfo);
            this.scheduler.unscheduleJob(triggerKey);
            this.schedulerJobInfoRepository.save(schedulerJobInfo);

            LOG.info(">>>>> jobName = [{}]{}", schedulerJobInfo.getJobName(),
                    schedulerJobInfo.getJobStatus().getDescription());

        } catch (final SchedulerException exception) {
            throw new FusionPlexException("Error encountered while attempting to unschedule the job:" + exception.getMessage());
        }
    }

    public void pauseJob(final UUID jobId) {
        final SchedulerJobInfo schedulerJobInfo = this.findEntityById(jobId);

        try {
            schedulerJobInfo.setJobStatus(JobStatus.PAUSED);

            if (!this.scheduler.isStarted()) {
                this.scheduler.start();
            }

            final JobKey jobKey = this.getJobKey(schedulerJobInfo);
            this.scheduler.pauseJob(jobKey);
            this.schedulerJobInfoRepository.save(schedulerJobInfo);

            LOG.info(">>>>> jobName = [{}]{}", schedulerJobInfo.getJobName(),
                    schedulerJobInfo.getJobStatus().getDescription());

        } catch (final SchedulerException exception) {
            throw new FusionPlexException("Error encountered while attempting to pause the job: " + exception.getMessage());
        }
    }

    public void resumeJob(final UUID jobId) {
        final SchedulerJobInfo schedulerJobInfo = this.findEntityById(jobId);

        try {
            schedulerJobInfo.setJobStatus(JobStatus.RESUMED);

            if (!this.scheduler.isStarted()) {
                this.scheduler.start();
            }

            final JobKey jobKey = this.getJobKey(schedulerJobInfo);
            this.scheduler.resumeJob(jobKey);
            this.schedulerJobInfoRepository.save(schedulerJobInfo);

            LOG.info(">>>>> jobName = [{}]{}", schedulerJobInfo.getJobName(),
                    schedulerJobInfo.getJobStatus().getDescription());

        } catch (final SchedulerException exception) {
            throw new FusionPlexException("Error encountered while attempting to resume the job: " + exception.getMessage());
        }
    }

    public void deleteJob(final UUID jobId) {
        final SchedulerJobInfo schedulerJobInfo = this.findEntityById(jobId);

        try {

            if (!this.scheduler.isStarted()) {
                this.scheduler.start();
            }

            final JobKey jobKey = this.getJobKey(schedulerJobInfo);
            final TriggerKey triggerKey = this.getTriggerKey(schedulerJobInfo);

            this.scheduler.unscheduleJob(triggerKey);

            this.scheduler.deleteJob(jobKey);
            this.schedulerJobInfoRepository.delete(schedulerJobInfo);

            LOG.info(">>>>> jobName = [{}] deleted", schedulerJobInfo.getJobName());

        } catch (final SchedulerException exception) {
            throw new FusionPlexException("Error encountered while attempting to delete the job: " + exception.getMessage());
        }
    }

    public void createOrUpdateJob(
            final SchedulerJobInfoDto dto) {
        this.validateSchedulerParameters(dto);

        final SchedulerJobInfo schedulerJobInfo = this.getModelMapper().map(dto, SchedulerJobInfo.class);

        schedulerJobInfo.setJobClass(dto.getJobClass().getClazz().getName());

        try {
            this.scheduleJob(schedulerJobInfo);

        } catch (final SchedulerException exception) {
            throw new FusionPlexException("Error encountered while attempting to create a new job: " + exception.getMessage());
        }
    }

    private void scheduleJob(final SchedulerJobInfo schedulerJobInfo) throws SchedulerException {

        final SchedulerJobInfo savedSchedulerJobInfo = schedulerJobInfo.getId() != null ? this.schedulerJobInfoRepository
                .findById(schedulerJobInfo.getId()).orElse(schedulerJobInfo) : schedulerJobInfo;

        if (savedSchedulerJobInfo.getId() != null) {
            savedSchedulerJobInfo.map(schedulerJobInfo);
        }

        savedSchedulerJobInfo.verifyIfCronJob();

        final TriggerKey triggerKey = TriggerKey
                .triggerKey(savedSchedulerJobInfo.getJobName(), savedSchedulerJobInfo.getJobGroup());

        if (this.scheduler.getTriggerState(triggerKey).equals(Trigger.TriggerState.ERROR)) {
            this.scheduler.resetTriggerFromErrorState(triggerKey);
        }

        final boolean isDurable = true;

        JobDetail jobDetail = ScheduledJobs.getJobDetail(
                schedulerJobInfo.getJobName(), schedulerJobInfo.getJobGroup(),
                savedSchedulerJobInfo.getDescription(), schedulerJobInfo.getJobClass(), isDurable);

        final Trigger trigger = this.createTrigger(schedulerJobInfo);

        if (!this.scheduler.checkExists(jobDetail.getKey())) {
            jobDetail = this.createJobDetail(schedulerJobInfo, isDurable);

            this.scheduler.scheduleJob(jobDetail, trigger);
            schedulerJobInfo.setJobStatus(JobStatus.SCHEDULED);
        } else {
            this.scheduler.deleteJob(jobDetail.getKey());
            this.scheduler.scheduleJob(jobDetail, trigger);
            savedSchedulerJobInfo.setJobStatus(JobStatus.RESCHEDULED);
        }

        this.schedulerJobInfoRepository.save(schedulerJobInfo);

        LOG.info(">>>>> jobName = [{}]{}", schedulerJobInfo.getJobName(),
                schedulerJobInfo.getJobStatus().getDescription());
    }

    private JobKey getJobKey(final SchedulerJobInfo schedulerJobInfo) {
        return JobKey.jobKey(schedulerJobInfo.getJobName(), schedulerJobInfo.getJobGroup());
    }


    private TriggerKey getTriggerKey(final SchedulerJobInfo schedulerJobInfo) {
        return TriggerKey.triggerKey(schedulerJobInfo.getJobName(), schedulerJobInfo.getJobGroup());
    }

    private Trigger createTrigger(final SchedulerJobInfo schedulerJobInfo) {
        if (schedulerJobInfo.isCronJob()) {
            return this.schedulerCreator.createCronTrigger(schedulerJobInfo.getJobName(),
                    schedulerJobInfo.getJobGroup(), schedulerJobInfo.getDescription(), new Date(),
                    schedulerJobInfo.getCronExpression(), SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW);
        } else {

            return this.schedulerCreator.createSimpleTrigger(schedulerJobInfo.getJobName(),
                    schedulerJobInfo.getJobGroup(), schedulerJobInfo.getDescription(),
                    FusionPlexUtil.localTimeToDate(schedulerJobInfo.getStartTime()),
                    schedulerJobInfo.getRepeatInterval(), schedulerJobInfo.getRepeatCount(),
                    SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW);
        }
    }

    private JobDetail createJobDetail(final SchedulerJobInfo schedulerJobInfo, final boolean isDurable) {

        return this.schedulerCreator.createJob(ScheduledJobs.getClass(schedulerJobInfo.getJobClass()),
                isDurable, this.applicationContext, schedulerJobInfo.getJobName(),
                schedulerJobInfo.getJobGroup(), this.jobParameters);
    }

    private SchedulerJobInfo findEntityById(final UUID jobId) {
        return this.schedulerJobInfoRepository.findById(jobId)
                .orElseThrow(() -> new FusionPlexException("Scheduled job Not Found!"));
    }

    private void validateSchedulerParameters(final SchedulerJobInfoDto dto) {

        if (dto.getRepeatCount() != null && dto.getRepeatCount() < 0) {
            throw new FusionPlexException("Repeat count must be greater than zero");
        }

        if (dto.getRepeatInterval() != null && dto.getRepeatInterval() < 0) {
            throw new FusionPlexException("Repeat interval must be greater than zero");
        }

        if ((dto.getRepeatCount() != null || dto.getRepeatInterval() != null || dto.getStartTime() != null)
                && (dto.getCronExpression() != null && !dto.getCronExpression().isEmpty())) {
            throw new FusionPlexException("Cron jobs cannot have repeat interval, repeat count or start time");
        }

    }

    private ModelMapper getModelMapper() {
        return new ModelMapper();
    }
}

Step 10: Creating the Scheduler Job

This class is what Quartz will execute when scheduled. To qualify as a Quartz job, it must either extend QuartzJobBean or implement the Job interface. You should use Job if you are not using Spring or don’t want to utilize Spring’s dependency injection. Use QuartzJobBean when you are running a Spring Boot application and want to take advantage of Spring’s dependency injection. For this tutorial, we will extend QuartzJobBean.

package io.softwarebuilding.fusionplex.jobs;

import io.softwarebuilding.fusionplex.component.ApplicationContextProvider;
import io.softwarebuilding.fusionplex.service.ClientService;
import org.jetbrains.annotations.NotNull;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.QuartzJobBean;

public class UpdateLatestMoviesJob extends QuartzJobBean {

    private static final Logger LOG = LoggerFactory.getLogger(UpdateLatestMoviesJob.class);

    @Override
    protected void executeInternal(@NotNull final JobExecutionContext context) throws JobExecutionException {
        LOG.info("Updating latest movies");

        ApplicationContext applicationContext = ApplicationContextProvider.getApplicationContext();
        ClientService clientService = applicationContext.getBean(ClientService.class);

        clientService.updateLatestPlayingMovies();

        LOG.info("Finished updating latest movies");
    }
}

Step 11: Creating the Controller Layer

The controller layer manages the interaction between the user and the application. It processes user requests, handles scheduling information, and ensures it is displayed appropriately on the screen. This layer acts as a bridge between the user interface and backend services, orchestrating the flow of data to and from the views.

package io.softwarebuilding.fusionplex.controller;

import io.softwarebuilding.fusionplex.dto.SchedulerJobInfoDto;
import io.softwarebuilding.fusionplex.service.SchedulerJobInfoService;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.util.UUID;

@Controller
@SessionAttributes("schedulerJob")
public class SchedulerController {

    private final SchedulerJobInfoService schedulerJobInfoService;

    @Autowired
    public SchedulerController(SchedulerJobInfoService schedulerJobInfoService) {
        this.schedulerJobInfoService = schedulerJobInfoService;
    }

    @GetMapping("/scheduler/manage")
    public String showManageSchedulerPage(Model model) {
        model.addAttribute("schedulerJobs", schedulerJobInfoService.findAll());
        return "manageSchedulers";
    }

    @GetMapping("/scheduler/create")
    public String showCreateSchedulerJobPage(Model model, HttpSession session) {
        SchedulerJobInfoDto dto = session.getAttribute("schedulerJob") != null ? 
            (SchedulerJobInfoDto) session.getAttribute("schedulerJob") : new SchedulerJobInfoDto();
        prepareModelForSchedulerForm(model, dto, false);
        return "createEditSchedulerJobs";
    }

    @GetMapping("/scheduler/edit/{id}")
    public String showEditSchedulerJobPage(@PathVariable UUID id, Model model) {
        SchedulerJobInfoDto dto = schedulerJobInfoService.findById(id);
        prepareModelForSchedulerForm(model, dto, true);
        return "createEditSchedulerJobs";
    }

    @PostMapping("/scheduler/save")
    public String saveSchedulerJob(@Valid @ModelAttribute("schedulerJob") SchedulerJobInfoDto dto,
                                   BindingResult result, Model model, RedirectAttributes redirectAttributes,
                                   SessionStatus sessionStatus, HttpSession session) {
        if (result.hasErrors()) {
            prepareModelForSchedulerForm(model, dto, false);
            session.setAttribute("errors", true);
            return "createEditSchedulerJobs";
        }
        schedulerJobInfoService.createOrUpdateJob(dto);
        sessionStatus.setComplete();
        session.removeAttribute("schedulerJob");
        session.removeAttribute("errors");
        redirectAttributes.addFlashAttribute("alertMessage", "Scheduler Job saved successfully");
        redirectAttributes.addFlashAttribute("alertType", "success");
        return "redirect:/scheduler/manage";
    }

    @PostMapping("/scheduler/delete/{id}")
    public String deleteSchedulerJob(@PathVariable UUID id, RedirectAttributes redirectAttributes) {
        schedulerJobInfoService.deleteJob(id);
        redirectAttributes.addFlashAttribute("alertMessage", "Scheduler Job deleted successfully");
        redirectAttributes.addFlashAttribute("alertType", "success");
        return "redirect:/scheduler/manage";
    }

    @PostMapping("/scheduler/unschedule/{id}")
    public String unscheduleSchedulerJob(@PathVariable UUID id, RedirectAttributes redirectAttributes) {
        schedulerJobInfoService.unscheduleJob(id);
        redirectAttributes.addFlashAttribute("

alertMessage", "Scheduler Job unscheduled successfully");
        redirectAttributes.addFlashAttribute("alertType", "success");
        return "redirect:/scheduler/manage";
    }

    @PostMapping("/scheduler/pause/{id}")
    public String pauseSchedulerJob(@PathVariable UUID id, RedirectAttributes redirectAttributes) {
        schedulerJobInfoService.pauseJob(id);
        redirectAttributes.addFlashAttribute("alertMessage", "Scheduler Job paused successfully");
        redirectAttributes.addFlashAttribute("alertType", "success");
        return "redirect:/scheduler/manage";
    }

    @PostMapping("/scheduler/resume/{id}")
    public String resumeSchedulerJob(@PathVariable UUID id, RedirectAttributes redirectAttributes) {
        schedulerJobInfoService.resumeJob(id);
        redirectAttributes.addFlashAttribute("alertMessage", "Scheduler Job resumed successfully");
        redirectAttributes.addFlashAttribute("alertType", "success");
        return "redirect:/scheduler/manage";
    }

    @PostMapping("/scheduler/shutdown")
    public String shutdownQuartzScheduler(RedirectAttributes redirectAttributes) {
        schedulerJobInfoService.shutdownScheduler();
        redirectAttributes.addFlashAttribute("alertMessage", "Scheduler shutdown successfully");
        redirectAttributes.addFlashAttribute("alertType", "success");
        return "redirect:/scheduler/manage";
    }

    @PostMapping("/scheduler/start")
    public String startQuartzScheduler(RedirectAttributes redirectAttributes) {
        schedulerJobInfoService.startScheduler();
        redirectAttributes.addFlashAttribute("alertMessage", "Scheduler initialized successfully");
        redirectAttributes.addFlashAttribute("alertType", "success");
        return "redirect:/scheduler/manage";
    }

    private void prepareModelForSchedulerForm(Model model, SchedulerJobInfoDto dto, boolean editMode) {
        CronJob selectedCronJob = dto.getCronJob() != null ? dto.getCronJob() : CronJob.NO;
        ScheduledJobs selectedJobClass = dto.getJobClass() != null ? dto.getJobClass() : null;
        String pageTitle = editMode ? "Edit Scheduler Job" : "Add New Scheduler Job";
        model.addAttribute("schedulerJob", dto);
        model.addAttribute("selectedCronJob", selectedCronJob);
        model.addAttribute("cronJobs", CronJob.values());
        model.addAttribute("jobClasses", ScheduledJobs.values());
        model.addAttribute("selectedJobClass", selectedJobClass);
        model.addAttribute("pageTitle", pageTitle);
        model.addAttribute("formAction", "/scheduler/save");
    }
}

Step 12: Thymeleaf Pages

The final step is to create Thymeleaf pages that enable users to create, update, pause, and resume jobs with the Quartz Scheduler.

Create ManageSchedulers.html

This page displays a list of all scheduler jobs and allows users to manage them directly.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/fragments :: head}">
</head>
<body>

<!-- Include the navbar fragment -->
<div th:replace="~{fragments/fragments :: navbar}"></div>

<!-- Include the alert fragment -->
<div th:replace="~{fragments/fragments :: alert}"></div>

<div class="container-fluid mt-5 d-flex flex-column justify-content-center align-items-center">
    <h2 class="text-center mt-5">Your Movie, TV Series, Documentary <span class="yellow-text">Catalogue Management</span></h2>
    <table class="table">
        <thead>
        <tr>
            <th scope="col">#</th>
            <th scope="col">Job Name</th>
            <th scope="col">Job Group</th>
            <th scope="col">Description</th>
            <th scope="col">Cron Expression</th>
            <th scope="col">Job Status</th>
            <th scope="col">Start Time</th>
            <th scope="col">Repeat Interval (seconds)</th>
            <th scope="col">Repeat Count</th>
            <th scope="col">Update Scheduler Status</th>
            <th scope="col">Actions</th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="scheduledJob, iterStat : ${schedulerJobs}">
            <th scope="row" th:text="${iterStat.count}">1</th>
            <td th:text="${scheduledJob.jobName}">Job Name</td>
            <td th:text="${scheduledJob.jobGroup}">Job Group</td>
            <td th:text="${scheduledJob.description}">Description</td>
            <td th:text="${scheduledJob.cronExpression}">Cron Expression</td>
            <td th:text="${scheduledJob.jobStatus}">Job Status</td>
            <td th:text="${scheduledJob.startTime}">Start Time</td>
            <td th:text="${scheduledJob.repeatInterval}">Repeat Interval</td>
            <td th:text="${scheduledJob.repeatCount}">Repeat Count</td>
            <td>
                <div class="d-inline-block">
                    <form method="post" th:action="@{/scheduler/pause/{id}(id=${scheduledJob.id})}">
                        <button class="btn btn-primary btn-sm" type="submit">Pause</button>
                    </form>
                </div>
                <div class="d-inline-block">
                    <form method="post" th:action="@{/scheduler/resume/{id}(id=${scheduledJob.id})}">
                        <button class="btn btn-success btn-sm" type="submit">Resume</button>
                    </form>
                </div>
                <div class="d-inline-block">
                    <form method="post" th:action="@{/scheduler/unschedule/{id}(id=${scheduledJob.id})}">
                        <button class="btn btn-info btn-sm" type="submit">Unschedule</button>
                    </form>
                </div>
            </td>
            <td>
                <div class="d-inline-block">
                    <form method="post" th:action="@{/scheduler/edit/{id}(id=${scheduledJob.id})}">
                        <button class="btn btn-warning btn-sm" type="submit">Edit</button>
                    </form>
                </div>
                <div class="d-inline-block">
                    <form method="post" th:action="@{/scheduler/delete/{id}(id=${scheduledJob.id})}">
                        <button class="btn btn-danger btn-sm" type="submit">Delete</button>
                    </form>
                </div>
            </td>
        </tr>
        </tbody>
    </table>
    <div class="d-flex justify-content-center gap-2">
        <a class="btn btn-dark mt-5" role="button" th:href="@{/scheduler/create}">Add New Scheduler</a>
        <form method="post" th:action="@{/scheduler/shutdown}">
            <button class="btn btn-danger mt-5" type="submit">Shutdown Scheduler</button>
        </form>
        <form method="post" th:action="@{/scheduler/start}">
            <button class="btn btn-success mt-5" type="submit">Initialize Scheduler</button>
        </form>
    </div

>
</div>
<div th:replace="~{fragments/fragments :: scripts}"></div>
</body>
</html>
Create CreateEditSchedulerJobs.html

This page allows users to create or edit individual scheduler jobs.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/fragments :: head}">
</head>
<body>

<!-- Include the navbar fragment -->
<div th:replace="~{fragments/fragments :: navbar}"></div>

<!-- Include the alert fragment -->
<div th:replace="~{fragments/fragments :: alert}"></div>

<div class="container-fluid mt-5 d-flex flex-column justify-content-center align-items-center">
    <h2 th:text="${pageTitle}">Manage Scheduler Jobs</h2>
    <form class="mt-5 w-50 needs-validation" method="post" th:action="@{${formAction}}" th:object="${schedulerJob}">
        <!-- Form fields are defined here -->
    </form>
</div>
<div th:replace="~{fragments/fragments :: scripts}"></div>
</body>
</html>
Update Fragments.html

Add essential links and scripts for UI components.

<link href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" rel="stylesheet">
<li class="nav-item">
    <a class="nav-link" th:href="@{/scheduler/manage}">Manage Schedules</a>
</li>
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
Update Script.js

Include JavaScript for dynamic form interactions.

flatpickr("#startTime", {
    enableTime: true,
    noCalendar: true,
    dateFormat: "H:i",
});

function toggleCronSettings() {
    const cronEnabled = document.getElementById('cronEnabled').value === 'true';
    const cronExpressionGroup = document.getElementById('cronExpressionGroup');
    const startTimeGroup = document.getElementById('startTimeGroup');
    const repeatIntervalGroup = document.getElementById('repeatIntervalGroup');
    const repeatCountGroup = document.getElementById('repeatCountGroup');

    if (cronEnabled) {
        cronExpressionGroup.style.display = 'block';
        startTimeGroup.style.display = 'none';
        repeatIntervalGroup.style.display = 'none';
        repeatCountGroup.style.display = 'none';
        clearFields(true);
    } else {
        cronExpressionGroup.style.display = 'none';
        startTimeGroup.style.display = 'block';
        repeatIntervalGroup.style.display = 'block';
        repeatCountGroup.style.display = 'block';
        clearFields(false);
    }
}

function clearFields(cronEnabled) {
    const cronExpression = document.getElementById('cronExpression');
    const startTime = document.getElementById('startTime');
    const repeatInterval = document.getElementById('repeatInterval');
    const repeatCount = document.getElementById('repeatCount');

    if (cronEnabled) {
        cronExpression.value = '';
        startTime.value = '';
        repeatInterval.value = '';
        repeatCount.value = '';
    } else {
        cronExpression.value = '';
    }
}

document.getElementById('cronEnabled').addEventListener('change', toggleCronSettings);
toggleCronSettings();

Conclusion

Congratulations on completing this tutorial on integrating and managing Quartz Scheduler with Spring Boot! Throughout this guide, you’ve equipped yourself with the knowledge to set up Quartz in a Spring environment, configure job stores, develop job classes, and create a responsive interface using Thymeleaf for job management.

This foundation will allow you to explore further enhancements, such as implementing advanced scheduling features or integrating with other systems and services. With the skills acquired, you can now create sophisticated scheduled tasks that are essential for managing complex business processes efficiently.

Happy coding!

Mobile Logo