Introduction to Quartz Job Scheduling Framework
- The "Hello, World" Quartz Project
- Scheduling the Quartz ScanDirectoryJob
- Scheduling a Quartz Job Declaratively
- Packaging the Quartz Application
Chapter 3. Hello, Quartz
Most readers will benefit from a small but realistic walkthrough of an example. As a writer, it takes some restraint to keep from unloading everything in a single chapter; as a reader, it takes some patience and a little leap of faith that the material is relevant and should be read, regardless of the simplicity of the example.
With those thoughts in mind, this chapter introduces a simple Quartz application that is representative of the type of applications that you'll create with the Quartz framework. This example walks you through the steps necessary to create and execute the sample application. When you're finished with this chapter, you should have a solid foundation to build upon for the rest of the book.
The "Hello, World" Quartz Project
This example application is a little more interesting than the proverbial System.out.println("Hello World from Quartz"). When Quartz executes a job, we expect it to perform some interesting and useful task on our behalf. That's what we are going to do here—something useful and interesting.
This chapter shows you how to create a Quartz job that, when told to do so by our Quartz application, will scan a specified directory and look for XML files. If it finds one or more XML files within the specified directory, it will print some cursory information about the file. How is this useful and interesting, you say? Hopefully, you can make the leap that after your job detects certain files in a directory, it can do many interesting things with those files. You might want to ftp them to a remote host or e-mail them as attachments. Perhaps the files are orders that were sent from a customer and need to be inserted into your database. Infinite possibilities exist; we discuss some of the more interesting ones later in the book.
All attempts have been made to keep this new material straightforward and to cover only the essentials. However, we also investigate some of the common configuration settings that affect the runtime behavior of a Quartz application. It's expected that you know the Java language well; we spend very little time explaining the standard aspects of it.
Finally, this chapter wraps up with a brief discussion of how to package the example application. Apache Ant is our choice for building and packaging Java projects: Quartz applications are no exception.
Setting Up the Quartz Project
The first step is to set up your development environment for this project. You can choose any development tool or IDE that makes you feel warm and fuzzy; Quartz won't hold it against you. If you're part of the growing mass of Java developers that have realized just how great Eclipse is, you'll be extra warm and fuzzy: We use that IDE throughout the examples in the book.
If you haven't already done so, you can download Eclipse from http://eclipse.org. You can choose any of the 3.x. For Eclipse documentation, check www.eclipse.org/eclipse/index.html; you should find everything you need to help you get started with Eclipse.
Configuring Quartz Within Eclipse
For the examples throughout this book, we create an Eclipse Java project; each chapter contains a separate source directory. Figure 3.1 illustrates the Quartz Java project in Eclipse.
Figure 3.1 Creating a Quartz Java project in Eclipse
You need to import several JARs into the project to build them successfully. To start, you need the Quartz binary, which is named quartz-<version>.jar. Quartz also requires several third-party libraries to work; the ones you need depend on which features of the framework you're using. For now, you should include the Commons Logging, Commons Collections, Commons BeanUtils, and Commons Digester libraries, found in the <QUARTZ_HOME>/lib/core and <QUARTZ_HOME>/lib/optional directories. Table 3.1 provides more information on the dependent JARs for Quartz.
It's also a good idea to pull the Quartz source code into Eclipse. This does two things for you. First, it enables you to set breakpoints and step from the code into Quartz source. Second, it helps you learn the framework from the inside out. If you need to see how something is (or isn't) working, you have the actual code right there at your fingertips. Try that with commercial software!
Creating the Quartz Job Class
Every Quartz job must have a concrete Java class that implements the org.quartz.Job interface. The Quartz job interface has a single method, execute(), that you must implement in your job. The execute() method signature is shown here:
public void execute(JobExecutionContext context) throws JobExecutionException;
When the Quartz Scheduler determines that it's time to fire a job, it instantiates the job class and invokes the execute() method. The Scheduler calls the execute() method with no expectations other than that you throw an org.quartz.JobExecutionException if there's a problem with the job.
You can perform whatever business logic you need within the execute() method: For example, you might instantiate and call a method on another class, send an e-mail, ftp a file, invoke a Web Service, call an EJB, execute a workflow, or, in the case of our example, check to see if files exist in a particular directory.
Listing 3.1 shows our first Quartz job, which is designed to scan a directory for files and display details about the files.
Example 3.1. The Example ScanDirectoryJob
package org.cavaness.quartzbook.chapter3; import java.io.File; import java.util.Date; import org.apache.commons.logging.Log; import Org.apache.commons.logging.LogFactory; import org.quartz.Job; import org.quartz.JobDataMap; import org.quartz.JobDetail; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; /** * <p> * A simple Quartz job that, once configured, will scan a * directory and print out details about the files found * in the directory. * </p> * Subdirectories will filtered out by the use of a * <code>{@link FileExtensionFileFilter}</code>. * * @author Chuck Cavaness * @see java.io.FileFilter */ public class ScanDirectoryJob implements Job { static Log logger = LogFactory.getLog(ScanDirectoryJob.class); public void execute(JobExecutionContext context) throws JobExecutionException { // Every job has its own job detail JobDetail jobDetail = context.getJobDetail(); // The name is defined in the job definition String jobName = jobDetail.getName(); // Log the time the job started logger.info(jobName + " fired at " + new Date()); // The directory to scan is stored in the job map JobDataMap dataMap = jobDetail.getJobDataMap(); String dirName = dataMap.getString("SCAN_DIR"); // Validate the required input if (dirName == null) { throw new JobExecutionException( "Directory not configured" ); } // Make sure the directory exists File dir = new File(dirName); if (!dir.exists()) { throw new JobExecutionException( "Invalid Dir "+ dirName); } // Use FileFilter to get only XML files FileFilter filter = new FileExtensionFileFilter(".xml"); File[] files = dir.listFiles(filter); if (files == null || files.length <= 0) { logger.info("No XML files found in " + dir); // Return since there were no files return; } // The number of XML files int size = files.length; // Iterate through the files found for (int i = 0; i < size; i++) { File file = files[i]; // Log something interesting about each file. File aFile = file.getAbsoluteFile(); long fileSize = file.length(); String msg = aFile + " - Size: " + fileSize; logger.info(msg); } } }
Let's take a closer look at what's going on in Listing 3.1.
When Quartz calls the execute() method, it passes it an org.quartz.JobExecutionContext, which wraps many things about the Quartz runtime environment and currently executing job. From the JobExecutionContext, you can access information about the Scheduler, the job, the trigger information for the job, and much, much more. In Listing 3.1, the JobExecutionContext is used to access the org.quartz.JobDetail class. The JobDetail class holds the detailed information about a job, including the name given to the job instance, the group that the job belongs to, whether the Job is persistent (volatility), and many other interesting properties.
The JobDetail has a reference to org.quartz.JobDataMap. The JobDataMap holds user-defined properties configured for the particular job. For example, in Listing 3.1, we get the name of the directory to scan from the JobDataMap. We could have hard-coded the directory in the ScanDirectoryJob, but we would have had a tough time reusing the job for other directories. In the later section "Scheduling a Quartz Job Programmatically," you'll see exactly how the directory is configured in the JobDataMap.
The rest of the code in the execute() method is just standard Java: It gets the directory name and creates a java.io.File object. It does a little bit of validation on the directory name to make sure it's a valid directory and that it exists. It then calls the listFiles() method on the File object to retrieve the files from the directory. java.io.FileFilter is created and passed in as an argument to the listFiles() method. org.quartzbook.cavaness. FileExtensionFileFilter implements the java.io.FileFilter interface to weed out directories and return only XML files. By default, the listFiles() method returns everything it finds, whether it's a file or a subdirectory, so the list must be filtered because we are interested only in XML files.
Listing 3.2 shows the FileExtensionFileFilter.
Example 3.2. The FileExtensionFileFilter Used in ScanDirectoryJob.java
package org.cavaness.quartzbook.chapter3; import java.io.File; import java.io.FileFilter; /** * A FileFilter that only passes Files of the specified extension. * <p> * Directories do not pass the filter. * * @author Chuck Cavaness */ public class FileExtensionFileFilter implements FileFilter { private String extension; public FileExtensionFileFilter(String extension) { this.extension = extension; } /* * Pass the File if it has the extension. */ public boolean accept(File file) { // Lowercase the filename for easier comparison String lCaseFilename = file.getName().toLowerCase(); return (file.isFile() && (lCaseFilename.indexOf(extension) > 0)) ? true:false; } }
The FileExtensionFileFilter is used to block any file that doesn't contain the string ".xml" as part of its name. It also blocks any subdirectories, which would normally be returned as well from the listFiles() method. Using file filters is a very convenient way of selectively choosing inputs to your Quartz jobs when they involve files as input.
In the next section, we discuss how to configure the job for scheduling and how to run the ScanDirectoryJob.