openxava / documentation / Job scheduling with Quartz

×News: OpenXava with AI - Refine the UI (Part 2) - December 1 · Read more

Video

In this video, you will learn how to schedule jobs in your OpenXava application using Quartz.

Any problem with this lesson? Ask in the forum

Code

You can copy the code used in the video here:

First, add the Quartz dependency to your pom.xml file:

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.2</version>
</dependency>

Create the web listener in src/main/java/com/yourcompany/projects/web/QuartzSchedulerListener.java:

package com.yourcompany.projects.web;

import javax.servlet.*;
import javax.servlet.annotation.*;

import org.apache.commons.logging.*;
import org.openxava.util.*;
import org.quartz.impl.*;

@WebListener
public class QuartzSchedulerListener implements ServletContextListener {
    
    private static final Log log = LogFactory.getLog(QuartzSchedulerListener.class);

    public void contextInitialized(ServletContextEvent sce) {
        try {
            StdSchedulerFactory.getDefaultScheduler().start();
        } 
        catch (Exception ex) {
            log.error(XavaResources.getString("quartz_scheduler_start_error"), ex);
        }
    }

    public void contextDestroyed(ServletContextEvent sce) {
        try {
            StdSchedulerFactory.getDefaultScheduler().shutdown();
        } 
        catch (Exception ex) {
            log.error(XavaResources.getString("quartz_scheduler_stop_error"), ex);
        }
    }
    
}

Create the job in src/main/java/com/yourcompany/projects/jobs/PlannedIssueReminderJob.java:

package com.yourcompany.projects.jobs;

import org.openxava.jpa.*;
import org.openxava.util.*;
import org.quartz.*;

import com.yourcompany.projects.model.*;

public class PlannedIssueReminderJob implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        try {
            String issueId = context.getJobDetail().getJobDataMap().getString("issue.id");
            Issue issue = XPersistence.getManager().find(Issue.class, issueId);
            String workerEmail = issue.getAssignedTo().getWorker().getEmail();
            
            Emails.send(
                workerEmail, 
                "REMINDER: " + issue.getTitle(), 
                issue.getDescription());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            XPersistence.commit();
        }
        
    }

}

Add the following methods to your Issue entity in the model package:

public void setPlannedFor(LocalDate plannedFor) {
    if (Is.equal(this.plannedFor, plannedFor)) return;
    if (this.plannedFor != null) unplanReminder();
    this.plannedFor = plannedFor;
    planReminder();      
}

private void planReminder() {
    try {
        if (plannedFor == null) return;
        if (getId() == null) return;
        JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.put("issue.id", getId());
        JobDetail job = JobBuilder.newJob(PlannedIssueReminderJob.class)
            .withIdentity(getId(), "issueReminders")
            .usingJobData(jobDataMap)  
            .build();

        LocalDateTime localDateTime = plannedFor.atStartOfDay();      
        Date date = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
                
        Trigger trigger = TriggerBuilder.newTrigger()
            .withIdentity(getId(), "issueReminders")
            .startAt(date)  
            .build();

        StdSchedulerFactory.getDefaultScheduler().scheduleJob(job, trigger);
    }
    catch (Exception e) {
        e.printStackTrace();
    }
}

private void unplanReminder() {
    try {
        StdSchedulerFactory.getDefaultScheduler().deleteJob(new JobKey(getId(), "issueReminders"));
    }
    catch (Exception e) {
        e.printStackTrace();
    }      
}

@PostPersist
private void planReminderIfNeeded() {
    if (plannedFor != null) planReminder();
}

Transcription

Hi, I'm Alexandra. If you dare to stay with me for a few minutes, I'll show you how to schedule any Java logic to run exactly when you need it. We'll do it through a practical example using the Quartz library to schedule jobs. The example is an OpenXava application, however this tutorial is generic enough to use these instructions in any Java application.

We'll do it in the application you're seeing, a simple project management application. But the steps we're going to take to schedule jobs can be followed with any of your OpenXava applications, whatever they are. Even in Java applications that are not OpenXava. Let's see what we want to achieve.

Let's go to the "My Calendar" module. We're going to add a task for the 21st of next month. We type "Go to the beach". Scrolling down we notice that the planned date is October 21st and that the task is assigned to Alexandra. Perfect.

In the current implementation, when saving the task, all these fields are simply stored and the task will be displayed in the calendar as well as in the issues list. The functionality we want to add is that when the 21st arrives, Alexandra receives an email reminding her that she has to go to the beach. We will modify the code of our application so that using Quartz it does this. But before that, let's take a look at what Quartz is.

With your browser, go to quartz-scheduler.org, the official Quartz page. Here you have a detailed explanation of what Quartz is, as well as access to documentation, downloads, and its source code. Quartz is possibly the most popular Java library for scheduling jobs, and it's also open source. Let's go to the IDE and get to work.

The first step is to add Quartz as a dependency in our project. For that, we open the pom.xml. We go to the dependencies section. And we start adding one. For the groupId we put org.quartz-scheduler. For the artifactId, quartz; and for the version: 2 point 3 point 2. Remember to do a Maven Install so that the library is downloaded and included in your project. In case you don't want to type, in the video description you'll find a link to a page with the code snippets so you can copy them.

Now that we have Quartz included in our project we need to make the application start it. Quartz is a kind of service or daemon that must always be running. In a web application, as is the case with OpenXava applications, the best way to start and stop Quartz is by placing it in a web listener, this way, whenever the application starts, Quartz also starts. Let's write our listener.

First we will create a package to place our web listener inside. We use the new package option in OpenXava Studio. We call the package 'web'. Inside the package, we will click to create a new class. We will call the class QuartzSchedulerListener, for example. And here we have the skeleton of our class. Let's convert this skeleton into a web listener.

To do this, we have annotated the class with @WebListener and made it implement ServletContextListener. Thus, the class must have contextInitialized, which executes when the application starts, and contextDestroyed, which executes when it stops. In the contextInitialized method, we add the line: StdSchedulerFactory.getDefaultScheduler().start() And in contextDestroyed: StdSchedulerFactory.getDefaultScheduler().shutdown() In this simple way, Quartz starts when our application starts and stops when the application stops. We now have Quartz running, let's now give it some job.

Now comes a crucial part: the definition of the jobs. First, we will create a package that we will call 'jobs'. The foundation of Quartz is jobs. A job is an object that contains the Java logic that will be executed at a given time. If you are an OpenXava programmer, think of it as an OpenXava action, but instead of executing when the user clicks a button, it executes at a specific time on a specific day. Without further ado, let's define our job.

We create a class called PlannedIssueReminderJob. A good name because it's for reminding planned tasks. We make it implement Job, from the org.quartz package. This requires it to have an execute method. Which we implement, for now with a humble print statement. This way, the message "Sending email..." will be printed to the console every time the job runs. The job will run at the times it is scheduled for; we will see how to schedule it later. For now, we assume it will run on the day the worker is supposed to receive the reminder. But first, we need to make our job do something useful.

Let's remove the print statement and send a real email. For that, we will use Emails, a utility included in OpenXava. We add the import. We specify: the email account, the subject, and the body of the message. But of course, we want to send the email to the appropriate person and include the task description. Where can we get that from? Well, from the job's context.

The job context is the argument of the execute method, to which we give the name it deserves. Now we can access job-specific information with: context.getJobDetail().getJobDataMap() This map contains arbitrary data that was assigned to the job when it was created and scheduled. When creating the job, we will assign it the id of the task that needs to be remembered, so that from here we can access it.

To access the task, the issue, we use JPA with OpenXava's XPersistence utility. From the Issue entity, we will obtain the title, description, and the worker's email. We will modify the call to Emails to send the correct data. We use workerEmail for the email, use issue.getTitle for the subject, and issue.getDescription for the message body. As a final detail, since we are using XPersistence and are not in an OpenXava action, we have to explicitly commit. And with this, our job is ready for battle. Now let's schedule it.

I'm going to show you the ideal place to schedule our job. Let's go to the Issue entity and look for plannedFor, which is the planned date. It's true that we could schedule the job in the save action, but it's better to modify the entity. This way, the job will be scheduled whenever the date is changed, no matter from where - from the calendar, from the issue maintenance, from a web service, from anywhere.

Lombok generates a hidden setter that simply assigns the value to the field. What we will do is override this setter to execute our own logic when plannedFor is changed, thus taking the opportunity to schedule the reminder. This would be our setter. We add the import for Is. If the date being assigned and the one we have match, we do nothing. If there is a previous date, we need to remove the reminder. We assign the new date and schedule the corresponding reminder. In summary, when the planned date for a task is changed, we remove the previous reminder, if any, and add a reminder scheduled for the new date. We still need to implement planReminder and unplanReminder. Let's do it.

This is the planReminder method. We press Control S to add the imports and we'll examine the method. If it doesn't have a plannedFor date, we do nothing, obviously. If the current Issue doesn't have an id, we also do nothing. This happens when the Issue hasn't been saved yet and its id hasn't been generated, which occurs when creating a new Issue. For this case, we have a solution that we'll see later.

The next step is to create the job. Notice that we first create a jobDataMap which will contain the data that can later be read in the job via the context. This is where we assign the value to issue.id that we read from the job. The value is the id of the current Issue. Then we create the job specifying that it should use our PlannedIssueReminderJob. We assign it an identifier which will be the current issue's id within the group "issueReminders". We set the jobDataMap and that's it.

To schedule the job we need a date of type java.util.Date. For this, we use these two lines, where we obtain that date from plannedFor, which is a LocalDate. Now we create the trigger with TriggerBuilder.newTrigger(). We specify the same job id we defined above and the date. The trigger indicates the point in time when we want to execute the job. In our case, we use a specific moment, but recurring times can be scheduled cyclically by hour, day, week, month, etc., similar to a UNIX cron job. And now we can schedule our job by calling scheduleJob with the job and the trigger. It's not that difficult - we create a job, create a trigger, and schedule it. Our planReminder method is now finished, let's move on to unplanReminder.

The unplanReminder method is much easier, we simply call deleteJob specifying the job identifier and that's it. There's still one detail left. What happens when a value is assigned to plannedFor for an Issue that hasn't been saved yet and therefore doesn't have an id? We solve this with a JPA PostPersist method. Simply, if after creating a new Issue it sees that it has a plannedFor date, it schedules the reminder. Well, that's it.

Now when the 21st arrives, Alexandra will receive an email reminding her that she has to go to the beach, all thanks to Quartz and your hard work. Let's summarize what we've done. First: we defined the dependency in the pom. Second: we started Quartz in the web listener. Third: we defined the job. Fourth: we scheduled the job.

This is a link to an article in the OpenXava documentation where you'll find the code snippets we wrote in this tutorial. The link is also in the video description. However, there's a faster way to get the source code, and that is by creating a project using the Project Management template. In OpenXava Studio, use the New OpenXava Project option. There, for the template choose Project Management.

The created project will include Quartz with logic to schedule reminders very similar to what we've developed in this tutorial, but with some improvements to make it more professional. The first is that in the job, that is PlannedIssueReminderJob, it includes a series of validations to make it more robust. The second is that the email message uses a template obtained from the i18n files, so we have a more professional and multilingual wording. The third is that we send messages to the log when validations fail, and we also send exceptions to the log, all using internationalized messages. There's no printStackTrace. The fourth is that it supports multi-company. It takes into account the active company and sends it as data to the job. The fifth is without a doubt the most important one: persistence.

As it is now, if the power goes out and we restart the application, all reminders will be lost—a serious issue. It's possible to configure Quartz to store the job scheduling in the database, which is done in the quartz.properties file and by creating some tables. The application generated from the template already includes the database configured and ready to store the scheduled tasks. If you've followed the steps up to this point, you already have reminders working in your application. Congratulations.

If not, we encourage you to create a project using the Project Management template or also using the corresponding Maven archetype, try it out and review the code. If you have any issues with the video, don't hesitate to ask us in the forum. Bye.