Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com Chapter 3 Hello Quartz Most readers 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 and 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. The chapter will walk you through the steps necessary to create and execute the example application. When finished, you should have a solid foundation from which to build upon for the rest of the chapters. The Hello World Quartz Project This example application will be a little more interesting than the proverbial System.out.println( “Hello World from Quartz”). When a job is executed by Quartz, we expect it to perform some interesting and useful task on our behalf. That’s what we are going to do, something useful and interesting. This chapter will show you how to create a Quartz job that will, when told to do so by our Quartz application, will scan a specified directory and look for any and all XML files. If one or more XML files are found within the specified directory, some cursory information about the file will be printed out. How is this useful and interesting, you say? Hopefully, you can make the leap that once your job is able to detect certain files in a directory; there are many interesting things to do with those files. You might want to FTP them to a remote host or Email them as attachments. Maybe the files are orders that were sent from a customer and need to be inserted into your database. There are infinite possibilities and we’ll discuss some of the more interesting ones later in the book. All attempts will be made to keep this new material straight-forward and cover only the essentials. However, we will be forced to 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 as very little time will be spent on explaining the standard aspects of it. Finally, this chapter will wrap up with a brief discussion of how to package the example application. Of course, Apache Ant is our choice for building and packaging Java projects and Quartz applications are no exception. Chapter 3, 1 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com Setting up the Quartz Project The first step is to get the development environment setup for this project. You may choose any development tool or IDE that makes you feel warm and fuzzy; Quartz won’t hold it against you. However, if you’re part of the growing mass of Java developers that have finally succumb to the peer pressure of other Eclipse users, then you’ll be extra warm and fuzzy as that’s the IDE we’ll be using throughout the examples in the book. However, if you are one of those stubborn types and refuse to be assimilated, don’t panic! Things will still make sense and you’ll be able to follow right along. Eclipse is mostly harmless and at the end of the day, it’s just Java, right? If you haven’t figured out it by now, you can download Eclipse from http://eclipse.org. You can choose any of the 3.X builds and you should be fine. For any and all Eclipse documentation, check this site: http://www.eclipse.org/eclipse/index.html. You should find everything you need to help you get started with Eclipse there. Configuring Quartz within Eclipse For the examples throughout this book, we’ll create an Eclipse Java project and each chapter will contain a separate source directory. Figure 3-1 illustrates the Quartz Java project in Eclipse. ***Insert Figure 3.1 03fig03.tif Figure 3.1 Creating a Quartz Java Project in Eclipse You will need to import several JARs into the project to build the projects successfully. For one, you will need the quartz.jar. For now, you can safely pull in the commons-logging.jar, commons-digester.jar and you should be fine for this chapter. Table 3.1 at the end of the chapter provides more information on the dependent jars for Quartz. It would also be a good idea if we pull the Quartz source code into eclipse. This will do two things for us. First, we’ll be able to set breakpoints and step from our code into Quartz source. The second benefit is that it will allow you to learn the framework from the inside out. If you need to see how something is or isn’t working, you’ll have the actual code right there are your fingertips. “Try that with commercial software!” Difference between a Quartz Application and a Job It’s worth pausing for a moment here for a small diversion to explain a confusing point for some. We’ll be using the phase “Quartz application” to refer to any software application that uses the Quartz framework as one of its included libraries. There may be (and usually is) many other libraries within the application, but if Quartz is present, we will consider it a Quartz application for the purposes of the discussions throughout the book. On the other hand, when we talk about a Quartz Job, we’re actually talking about a specific Java class that performs some task. There are normally many different types of Jobs within a Quartz application, each one with a concrete Java class. The two are not used interchangeably. Chapter 3, 2 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com 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 your job, it instantiates your Job class and invokes the execute() method. The execute() method is called by the Scheduler with no expectations other than you throw a org.quartz.JobExecutionException in the event there’s a problem with the Job. Therefore you are free to perform whatever business logic you need within the execute() method. A few examples may help to enlighten this point. You are free to instantiate and call a method on another Class, you can send an email, 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 below shows our first Quartz Job which is designed to scan a directory for files and display details about the files. Listing 3.1 The Example ScanDirectoryJob package org.cavaness.quartzbook.chapter3; import java.io.File; import java.util.Date; import import import import import import import org.apache.commons.logging.Log; org.apache.commons.logging.LogFactory; org.quartz.Job; org.quartz.JobDataMap; org.quartz.JobDetail; org.quartz.JobExecutionContext; org.quartz.JobExecutionException; public class ScanDirectoryJob implements Job { static Log logger = LogFactory.getLog(ScanDirectoryJob.class); public void execute(JobExecutionContext context) throws JobExecutionException { // Every job has it's 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 diretory to scan is stored in the job map JobDataMap dataMap = jobDetail.getJobDataMap(); Chapter 3, 3 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com String directoryName = dataMap.getString("SCAN_DIR"); // Validate the required input if (directoryName == null) { throw new JobExecutionException( "Directory not configured"); } // Make sure the diretory exists File dir = new File(directoryName); if (!dir.exists()) { throw new JobExecutionException( "Invalid Dir " + directoryName); } // Use FileFilter to get only XML files File[] files = dir.listFiles(new XMLFileOnlyFileFilter()); 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. * You can imagine how we could do many things * with this file, like email, ftp or parse and * store into db. */ String msg = file.getAbsoluteFile() + " - Size: " + file.length(); logger.info(msg); } } } Let’s take a closer look at what’s going on in Listing 3.1 When Quartz calls your execute() method, it passes it a org.quartz.JobExecutionContext that 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. The JobDetail class is a wrapper around an instance of your concrete Job. It holds a reference to the Job instance, as well as other information such as the Chapter 3, 4 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com name given to the Job instance, the group that the Job belongs to, whether or not the Job is persistent (volatility) and many other interesting properties. The JobDetail has an association to the org.quartz.JobDataMap, which wraps a java.util.Map and 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. Later in the 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. There is a java.io.FileFilter created and passed in as an argument to the listFiles() method. The org.quartzbook.cavaness.XMLFileOnlyFileFilter implements the java.io.FileFilter interface in order to weed out directories and only return 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 since we are only interested in XML files. Note The XMLFileOnlyFileFilter is not part of the Quartz framework. It’s a subclass of the java.io.FileFilter, which is part of core Java. We have created the XMLFileOnlyFileFilter as part of our example to filter out everything but XML files. It’s quite handy and you should think about building a set of file filters for your application and reusing them for your various Quartz jobs. The XMLFileOnlyFileFilter is shown in Listing 3.2. Listing 3.2 The XMLFileOnlyFileFilter used in ScanDirectoryJob.java package org.cavaness.quartzbook.chapter3; import java.io.File; import java.io.FileFilter; public class XMLFileOnlyFileFilter implements FileFilter { /* * Pass the File if it's an XML file. */ public boolean accept(File file) { // Lowercase the filename for easier comparison String lCaseFilename = file.getName().toLowerCase(); return (file.isFile() && (lCaseFilename.indexOf(".xml") > 0)) ? true : false; } } Chapter 3, 5 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com The XMLFileOnlyFileFilter is used to block any file (or sub-directories) that doesn’t contain the string “.xml” as part of its name. This is not perfect, we could actually inspect the internals of the file to make sure that it’s XML, but the XMLFileOnlyFileFilter will be fine for this example. Using file filters this way is a very convenient way of selectively choosing inputs to your Quartz jobs when they involve files as input. Declarative versus Programmatic Configuration With Quartz, there are two approaches to configuring the runtime aspects of an application: 1) Declarative and 2) Programmatic. For some aspects of the framework, it only makes sense to use external configuration files, as we all have learned that hard-coding settings within our software has its limitations. For other aspects of your Quartz application, you will need to make some decisions based on the requirements and functionality you need from Quartz. As you’ll see in the next section, the places where a declarative or a programmatic decision needs to be made will be highlighted for you. As much of the Java industry is moving towards a declarative approach, this will be our preference. In the next section, we’ll discuss how to configure the Job for scheduling and how to run the ScanDirectoryJob. Scheduling the Quartz ScanDirectoryJob So far, we’ve created a Quartz job but we haven’t discussed what to do with it. We obviously need a way to set a schedule for the job to run. The schedule may be a one time event or we might need the job to run every night at midnight, except on Sunday. As the previous chapter highlighted, the Quartz Scheduler is the heart and soul of the framework. All jobs are registered with the scheduler and when necessary, the scheduler creates an instance of the Job class and invokes the execute() method. Scheduler Creates New Job Instances for Each Execution The scheduler creates a new instance of the Job class for each and every execution. This means that any state that you have in instance variables is lost between job executions. There is a way to persist job state from one invocation to another, but it’s through the use of the JobDataMap, which we’ll talk about later in this chapter. When creating a stateful job, there are a few issues that you must understand. Most importantly is that no two jobs of the same type can execute concurrently. So you want to be careful when choosing jobs to be stateful. We’ll discuss these issues in more depth in Chapter X. Chapter 3, 6 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com Creating and Running the Quartz Scheduler Before we talk about the ScanDirectoryJob specifically, let’s discuss in general how to instantiate and run an instance of the Quartz scheduler. Listing 3.3 illustrates the necessary steps to create a Scheduler and start it running. Listing 3.3 Running a Simplified Quartz Scheduler package org.cavaness.quartzbook.chapter3; import java.util.Date; import import import import import org.apache.commons.logging.Log; org.apache.commons.logging.LogFactory; org.quartz.Scheduler; org.quartz.SchedulerException; org.quartz.impl.StdSchedulerFactory; public class RunSimpleScheduler { // Commons Logger static Log logger = LogFactory.getLog(RunSimpleScheduler.class); public static void main(String[] args) { RunSimpleScheduler simple = new RunSimpleScheduler(); simple.runSimpleScheduler(); } public void runSimpleScheduler() { Scheduler scheduler = null; try { // Get a Scheduler instance from the Factory scheduler = StdSchedulerFactory.getDefaultScheduler(); // Start the scheduler scheduler.start(); logger.info("SimpleScheduler was started…” ); logger.info( new Date() ); } catch (SchedulerException ex) { // deal with any exceptions logger.error(ex); } } } If you run the code in Listing 3.3 and you are logging the output, you should see something like the following output: INFO [main] (RunSimpleScheduler.java:29) - SimpleScheduler was started INFO [main] (RunSimpleScheduler.java:29) - Thu Jun 09 11:36:48 EDT 2005 Chapter 3, 7 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com Turning off Quartz Info Log Messages If you are using the Commons Logging framework along with Log4J as the examples throughout this book are, you may need to turn off info log messages for everything but the examples in this book. This is because Quartz logs a sizable amount of debug and info messages and once you understand what Quartz is doing, the messages that you may care more about, can get lost in the volumes of log messages. You can do this by setting the default logger to log only errors and setting the messages that come from the example packages to show info. Here’s an example log4j.properties file that will accomplish this: # Create stdout appender log4j.rootLogger=error, stdout # Configure the stdout appender to go to the Console log4j.appender.stdout=org.apache.log4j.ConsoleAppender # Configure stdout appender to use the PatternLayout log4j.appender.stdout.layout=org.apache.log4j.PatternL ayout # Pattern output the caller's filename and line # log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n # Print only messages of level INFO or above log4j.logger.org.cavaness.quartzbook=INFO This log4j.properties file will log only error messages to stdout, but any info messages coming from the package org.cavaness.quartzbook will be seen. This is due to the last line in the properties file. If you would also like to see debug messages from the quartzbook classes, then change INFO to DEBUG. As simply as the code in Listing 3.3 seems, you have a running Quartz scheduler. Once the scheduler is up and running, there are some things you can do with it or information that you can obtain from it. For example, you might need to pause the scheduler, change some of the settings and/or jobs and then restart it. This is illustrated in the modifyScheduler() method fragment shown in Listing 3.4. Listing 3.4 Pausing and Restarting the Scheduler private void modifyScheduler(Scheduler scheduler) { try { // pause the scheduler if (!scheduler.isPaused()) { // pause the scheduler scheduler.pause(); Chapter 3, 8 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com } // Make changes to a job, etc. // and then restart it scheduler.start(); } catch (SchedulerException ex) { logger.error(ex); } } The partial fragment in Listing 3.4 is just a quick illustration that once you have a reference to a Quartz scheduler, there are some pretty interesting things you can do with it. We will add to our repertoire throughout the book. The culmination of which exists inside the Quartz Web Application discussed in Chapter X. As simple as these examples seem to be, there is a catch. Mainly that we haven’t specified any jobs to be executed or times for those jobs to execute. While the Quartz Scheduler did start up and run in Listing 3.4, we didn’t specify any jobs to be executed. And that’s what we’re going to discuss next. Scheduling a Quartz Job Programmatically All jobs that we would like to be executed by Quartz need to be registered with the Scheduler. In most situations, this should be done prior to the starting of the scheduler. As promised earlier in this chapter, this is one of those areas of Quartz that can be done in either a declarative or programmatic manner. First, you’ll see how to do it programmatically and then later in this chapter we’ll repeat this exercise with the declaratively approach. For each job that is to be registered with the scheduler, a JobDetail needs to be created and attached to the scheduler instance. This is shown in Listing 3.5 below. Listing 3.5 Scheduling a Job Programmatically private void scheduleJob(Scheduler scheduler) { try { // Create a new JobDetail JobDetail jobDetail = new JobDetail("ScanDirectory", Scheduler.DEFAULT_GROUP, org.quartzbook.chapter3.ScanDirectoryJob.class); // Set the directory to scan in our example jobDetail .getJobDataMap() .put("SCAN_DIR", "c:\\quartz-book\\input"); /* * Setup a trigger to start firing now, with a null end * date/time, repeat forever and have 10 secs (10000 ms) Chapter 3, 9 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com * between each firing. */ SimpleTrigger trigger = new SimpleTrigger("scanTrigger", Scheduler.DEFAULT_GROUP, new Date(), null, SimpleTrigger.REPEAT_INDEFINITELY, 10000L); // Associate the trigger with the job in the scheduler scheduler.scheduleJob(jobDetail, trigger); } catch (SchedulerException ex) { logger.error(ex); } } The scheduleJob() method in Listing 3.5 is part of our example, not part of Quartz. It’s not a complete example yet, but just the essentials to show how a Job is setup and registered with the scheduler. The code in this listing creates a JobDetail object for the Job that we wish to run. The arguments in the constructor for the JobDetail include a name given to the Job, a logical group to assign the Job to and the fully-qualified class that implements the org.quartz.Job interface. There are a few constructors for the JobDetail class: public JobDetail(); public JobDetail(String name, String group, Class jobClass); public JobDetail(String name, String group, Class jobClass, boolean volatility, boolean durability, boolean recover); Note A Job should be uniquely identifiable by its name and group within a scheduler. If you add two jobs with the same name and group, the first job may be overwritten or an ObjectAlreadyExistsException will be thrown, depending on the settings for the Job Store. This will be discussed further in the next Chapter. For more detailed information on the Quartz API, see Appendix A. As stated earlier in this Chapter, the JobDetail acts as a wrapper for a specific job. It contains properties for the job instance and also can be accessed by the Job class at runtime. One of the most important uses of the JobDetail is to hold the JobDataMap, which is used to store state for a specific Job instance. In listing 3.5, the name of the directory to scan is stored in the JobDataMap. Note Go back to Listing 3.1 and look at the code where the scan directory property is retrieved from the JobDataMap. Based on Listing 3.5, you can now see how it gets set. The next thing we see in listing 3.5 is the use of the org.quartz.SimpleTrigger class. Triggers are discussed in much more detail later in the book, but suffice to say that Triggers are the way that the Quartz scheduler knows when to fire the registered jobs. As you can see from listing 3.5, a SimpleTrigger (which is a concrete subclass of org.quartz.Trigger) is created and associated with the JobDetail and registered wit the scheduler instance. There are several constructors for the SimpleTrigger, depending on your needs: Chapter 3, 10 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com public SimpleTrigger(); public SimpleTrigger(String name, String group); public SimpleTrigger(String name, String group, int repeatCount, long repeatInterval); public SimpleTrigger(String name, String group, Date startTime); public SimpleTrigger(String name, String group, Date startTime, Date endTime, int repeatCount, long repeatInterval); public SimpleTrigger(String name, String group, String jobName, String jobGroup, Date startTime, Date endTime, int repeatCount, long repeatInterval); Listing 3.5 illustrates how to register a single job with the scheduler. If you have more than one job (and you probably do), you will need to create a JobDetail for each job. Each one will need to be registered with the scheduler via the schedulerJob() method. SimpleTrigger versus CronTrigger There are two types of Triggers within Quartz: SimpleTrigger and CronTrigger. A SimpleTrigger is the simpler of the two and is used primarily to fire single event Jobs. Those that fire at some given time, repeat for n number of times with a delay of m between each firing and then quit. CronTriggers are much more complicated and powerful. They are based on a Calendar and are used when you need to execute a Job every Monday, Wednesday and Friday at midnight. This chapter will only focus on the SimpleTrigger. CronTriggers will be covered later. If you are reusing a job Class to run multiple instances of the same Job class, you need to create a JobDetail for each one. For example, if we wanted to reuse the ScanDirectoryJob to check two different directories, we would need to create and register two instances of the JobDetail class. Listing 3.6 shows how this could be done. Listing 3.6 Creating Multiple Instances of the ScanDirectoryJob package org.cavaness.quartzbook.chapter3; import java.util.Date; import import import import import import import import org.apache.commons.logging.Log; org.apache.commons.logging.LogFactory; org.quartz.JobDetail; org.quartz.Scheduler; org.quartz.SchedulerException; org.quartz.SimpleTrigger; org.quartz.Trigger; org.quartz.impl.StdSchedulerFactory; public class RunMultipleScanDirectoryJobs { Chapter 3, 11 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com static Log logger = LogFactory.getLog(RunMultipleScanDirectoryJobs.class); public static void main(String[] args) { RunMultipleScanDirectoryJobs simple = new RunMultipleScanDirectoryJobs(); simple.runScheduler(); } public void runScheduler() { Scheduler scheduler = null; try { // Get a Scheduler instance from the Factory scheduler = StdSchedulerFactory.getDefaultScheduler(); /* * Setup a trigger to start firing now, with a null end date/time, * repeat forever and have 10 secs (10000 ms) between each firing. */ SimpleTrigger trigger1 = new SimpleTrigger("scanTrigger1", Scheduler.DEFAULT_GROUP, new Date(), null, SimpleTrigger.REPEAT_INDEFINITELY, 10000L); // Register first instance of ScanDirectoryJob scheduleScanDirectoryJob(scheduler, "ScanDirectory", "c:\\quartz-book\\input1", trigger1); // Create another instance of a trigger that fires every 15 secs SimpleTrigger trigger2 = new SimpleTrigger("scanTrigger2", Scheduler.DEFAULT_GROUP, new Date(), null, SimpleTrigger.REPEAT_INDEFINITELY, 15000L); // Register another instance of ScanDirectoryJob scheduleScanDirectoryJob(scheduler, "ScanDirectory2", "c:\\quartz-book\\input2", trigger2); // Start the scheduler scheduler.start(); logger.info("Scheduler was started at " + new Date()); } catch (SchedulerException ex) { // deal with any exceptions logger.error(ex); } } private void scheduleScanDirectoryJob(Scheduler scheduler, String jobName, String scanDir, Trigger trigger) { try { Chapter 3, 12 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com JobDetail jobDetail = new JobDetail(jobName, Scheduler.DEFAULT_GROUP, org.cavaness.quartzbook.chapter3.ScanDirectoryJob.class); // Add the directory to scan jobDetail.getJobDataMap().put("SCAN_DIR", scanDir); // Associate the trigger with the job in the scheduler scheduler.scheduleJob(jobDetail, trigger); } catch (SchedulerException ex) { logger.error(ex); } } } Listing 3.6 is pretty straightforward. The runScheduler() method creates a Trigger and invokes the scheduleScanDirectoryJob() method. The scheduleScanDirectoryJob() method creates a new JobDetail each time it is invoked, sets the scan directory and registers the JobDetail and Trigger with the scheduler instance. Note We could have passed in enough information to the scheduleScanDirectory() method so that it could create the Triggers. This would have made the runScheduler() method a little cleaner and isolated our dependency on Quartz to the single method. Running the RunMultipleScanDirectoryJobs Quartz Jobs If we execute the RunMultipleScanDirectoryJobs class, we should get output similar to the following: INFO [main] (RunMultipleScanDirectoryJobs.java:53) - Scheduler was started at Thu Jun 09 12:34:28 EDT 2005 INFO [DefaultQuartzScheduler_Worker-0] (ScanDirectoryJob.java:46) - ScanDirectory1 fired at Thu Jun 09 12:34:28 EDT 2005 INFO [DefaultQuartzScheduler_Worker-8] (ScanDirectoryJob.java:46) - ScanDirectory2 fired at Thu Jun 09 12:34:28 EDT 2005 INFO [DefaultQuartzScheduler_Worker-9] (ScanDirectoryJob.java:46) - ScanDirectory1 fired at Thu Jun 09 12:34:38 EDT 2005 INFO [DefaultQuartzScheduler_Worker-0] (ScanDirectoryJob.java:46) - ScanDirectory2 fired at Thu Jun 09 12:34:43 EDT 2005 Chapter 3, 13 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com Scheduling a Quartz Job Declaratively As discussed earlier, we would like to handle configuring our software declaratively, rather than programmatically, as much as possible. If we look back at Listing 3.6, if we needed to change the time that the Jobs started, or the frequency, we will have to modify source and recompile. This is fine for these small example applications, but once you have a system that is large and complex, this quickly become a problem. So if there’s a way to schedule our Quartz jobs declaratively and our requirements allow for it, you should choose that approach. Before we can discuss how to schedule your jobs declaratively, we need to talk about the quartz.properties file. This properties file will allow us to configure the runtime environment of Quartz (without the file, defaults are used) and more importantly, tell Quartz to get the job schedules from an external resource. Configuring the quartz.properties File The quartz.properties file defines the runtime behavior of the Quartz application and contains many settings that can be set to control how Quartz behaves. Only the basics will be covered within this chapter with the more advanced settings saved for later. We will also not go in detail about the valid values for each setting, saving that for later as well. Note The Quartz framework sets defaults for almost all of these properties. Chapter X will provide more detail. Let’s look at a bare-bones quartz.properties file and discuss some of the settings. Listing 3.7 shows a very stripped down version of the quartz.properties file. Listing 3.7 Basic Quartz Properties File #=============================================================== # Configure Main Scheduler Properties #=============================================================== org.quartz.scheduler.instanceName = QuartzScheduler org.quartz.scheduler.instanceId = AUTO #=============================================================== # Configure ThreadPool #=============================================================== org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount = 5 org.quartz.threadPool.threadPriority = 5 #=============================================================== # Configure JobStore #=============================================================== org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore #=============================================================== # Configure Plugins Chapter 3, 14 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com #=============================================================== org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.JobInitializationPlugin org.quartz.plugin.jobInitializer.overWriteExistingJobs = true org.quartz.plugin.jobInitializer.failOnFileNotFound = true org.quartz.plugin.jobInitializer.validating=false In the quartz.properties file listed in Listing 7.3, the properties are arbitrarily separated into four sections. This is a logical separation for the sake of this discussion, the properties don’t have to be grouped or listed in any order. And obviously the lines with the # are comments. Note This discussion does not list every possible setting. We are only going to discuss here the basic settings that you need to be familiar or that are necessary to get our declarative example working. To see a complete listing of settings and their possible values, see Chapter X. Scheduler Properties The first section contains two lines and set the name and id for the Scheduler. For most applications, these settings and their associated values don’t matter. That is, they don’t matter to your application, but they are set in Quartz and used by the framework. Setting the instanceId value to AUTO allows Quartz to automatically generate id’s for each Scheduler instance. ThreadPool Properties The next section sets the necessary values for the Threads that run in the background and do the heavy lifting in Quartz. The threadCount property control how many worker threads are created and available to process jobs. In principal, the more jobs that you have to process, the more worker threads you’ll need. The number for the threadCount must be at least one but has a maximum of 200. The threadPriority property sets the priority that the worker threads run with. Threads with higher priorities are typically given preference over threads with a lower priority. The minimum value for the threadPriority is the constant Thread.MAX_PRIORITY, which equates to 10. The minimum value is Thread.MIN_PRIORITY which equals 1. The typical value for this property is Thread.NORM_PRIORITY which is 5. For most situations, you’ll want to set this to the Thread.NORM_PRIORITY value. JobStore Settings The properties within the JobStore section describe how the job and trigger information is stored during the lifecycle of the process. We haven’t really talked about the JobStore and its purpose. We will save that for later as it’s not necessary for our current example. For now, all you need to know is that we are storing our Job information in memory – this is opposed to a database. Storing our Job information in memory is fast and the easiest to configure, but when the Scheduler process is halted, all Job and trigger state is lost. Job storage in memory is accomplished by setting the org.quartz.jobStore.class property to org.quartz.simpl.RAMJobStore as we’ve done in Listing 3.7. If we don’t want to lose our job state if the JVM is halted, we could use a relational database to store that information. This Chapter 3, 15 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com requires a different JobStore implementation that we will discuss later. Chapter X will talk about the various types of JobStores and when you should be using one over the other. Plugin Settings The final section in our simple quartz.properties file is the one that specifies any Quartz plugins that we want to configure. A plugin is an approach used by many other open source frameworks such as Struts from Jakarta (http://struts.apache.org). The idea is to declaratively extend the framework by adding Classes that implement the org.quartz.spi.SchedulerPlugin interface. The SchedulerPlugin interface has three methods that get called by the Scheduler on startup and shutdown. Plugins for Quartz are discussed in detail in Chapter X. To declaratively configure our Job information in our example, we will be using a plugin called org.quartz.plugins.xml.JobInitializationPlugin that comes with Quartz. Be default, this plugin searches for a file called quartz_jobs.xml in the classpsath and loads job and trigger information from it. If we didn’t provide this job information in our source and had not installed the plugin, the Scheduler would start but not have any jobs to execute, as we saw back in Listing 3.3. The next section discusses the quartz_jobs.xml file, which we’ll informally refer to as the Job definition file. Note By default, the JobInitializationPlugin looks for a file called quartz_jobs.xml on your classpath. You can override this and force it to look for and use a file with a different name. In order to do this, you will need to set the filename in the quartz.properties file that we discussed in the previous section. For now, are going to just rely on the default filename quartz_jobs.xml and show you how to modify the quartz.properties file later in this chapter. Using the quartz_jobs.xml File Listing 3.8 shows the Job Definition XML file for our Scan Directory example. It configures job and trigger information using a declaratively approach exactly like Listing 3.5 programmatically. Let’s display the listing and we’ll walk through it briefly after you have a chance to look at it. Listing 3.8 The quartz_ jobs.xml file for the ScanDirectory Job <?xml version='1.0' encoding='utf-8'?> <quartz xmlns="http://www.quartzscheduler.org/ns/quartz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.quartzscheduler.org/ns/quartz http://www.quartzscheduler.org/ns/quartz/job_scheduling_data_1_1.xsd" version="1.1"> Chapter 3, 16 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com <calendar class-name="org.quartz.impl.calendar.HolidayCalendar" replace="true"> <name>holidayCalendar</name> <description>HolidayCalendar</description> <base-calendar classname="org.quartz.impl.calendar.WeeklyCalendar"> <name>weeklyCalendar</name> <description>WeeklyCalendar</description> <base-calendar classname="org.quartz.impl.calendar.AnnualCalendar"> <name>annualCalendar</name> <description>AnnualCalendar</description> </base-calendar> </base-calendar> </calendar> <job> <job-detail> <name>ScanDirectory</name> <group>DEFAULT</group> <description>A job that scans a directory for files</description> <job-class> org.cavaness.quartzbook.chapter3.ScanDirectoryJob </job-class> <volatility>false</volatility> <durability>false</durability> <recover>false</recover> <job-data-map allows-transient-data="true"> <entry> <key>SCAN_DIR</key> <value>c:\quartz-book\input</value> </entry> </job-data-map> </job-detail> <trigger> <simple> <name>scanTrigger</name> <group>DEFAULT</group> <job-name>ScanDirectory</job-name> <job-group>DEFAULT</job-group> <start-time>2005-06-10 6:10:00 PM</start-time> <!-- repeat indefinitely every 10 seconds --> <repeat-count>-1</repeat-count> <repeat-interval>10000</repeat-interval> </simple> </trigger> </job> </quartz> There are two top-level elements within the job definition file in Listing 3.8: the <calendar> and the <job> elements. Note Chapter 3, 17 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com We won’t go into all of the details for the job definition file in this chapter. For detailed information, see the Plugin Chapter X. Quartz utilizes the <calendar> element to restrict or eliminate certain dates and/or times from the Job’s execution. Chapter X will discuss using calendars in detail. For our example here, we are going to stick with a standard calendar. The <job> element represents a job that you want to register with the scheduler, just as we did earlier in the chapter with the scheduleJob() method. You can see the <job-detail> and <trigger> elements, which we programmatically passed into the schedulerJob() method in Listing 3.5. This is essentially what is happening here, just in a declarative fashion. You can also see in Listing 3.8 that we set the SCAN_DIR property as we also did in the Listing 3.5 example. The <trigger> element is also very intuitive. It simply sets up a SimpleTrigger with the same properties as before. So, Listing 3.8 is just a different (arguably better) way of doing what we did in Listing 3.5. You obviously can support multiple jobs as well. We did this in Listing 3.6 programmatically and we can also support it declaratively as we do here in Listing 3.9. Listing 3.9 Using Multiple Jobs in the quartz_jobs.xml File <?xml version='1.0' encoding='utf-8'?> <quartz xmlns="http://www.quartzscheduler.org/ns/quartz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.quartzscheduler.org/ns/quartz http://www.quartzscheduler.org/ns/quartz/job_scheduling_data_1_1.xsd" version="1.1"> <calendar class-name="org.quartz.impl.calendar.HolidayCalendar" replace="true"> <name>holidayCalendar</name> <description>HolidayCalendar</description> <base-calendar classname="org.quartz.impl.calendar.WeeklyCalendar"> <name>weeklyCalendar</name> <description>WeeklyCalendar</description> <base-calendar classname="org.quartz.impl.calendar.AnnualCalendar"> <name>annualCalendar</name> <description>AnnualCalendar</description> </base-calendar> </base-calendar> </calendar> <job> <job-detail> <name>ScanDirectory1</name> <group>DEFAULT</group> <description>A job that scans a directory for files</description> <job-class> org.cavaness.quartzbook.chapter3.ScanDirectoryJob Chapter 3, 18 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com </job-class> <volatility>false</volatility> <durability>false</durability> <recover>false</recover> <job-data-map allows-transient-data="true"> <entry> <key>SCAN_DIR</key> <value>c:\quartz-book\input1</value> </entry> </job-data-map> </job-detail> <trigger> <simple> <name>scanTrigger1</name> <group>DEFAULT</group> <job-name>ScanDirectory1</job-name> <job-group>DEFAULT</job-group> <start-time>2005-06-10 6:10:00 PM</start-time> <!-- repeat indefinitely every 10 seconds --> <repeat-count>-1</repeat-count> <repeat-interval>10000</repeat-interval> </simple> </trigger> </job> <job> <job-detail> <name>ScanDirectory2</name> <group>DEFAULT</group> <description>A job that scans a directory for files</description> <job-class> org.cavaness.quartzbook.chapter3.ScanDirectoryJob </job-class> <volatility>false</volatility> <durability>false</durability> <recover>false</recover> <job-data-map allows-transient-data="true"> <entry> <key>SCAN_DIR</key> <value>c:\quartz-book\input2</value> </entry> </job-data-map> </job-detail> <trigger> <simple> <name>scanTrigger2</name> <group>DEFAULT</group> <job-name>ScanDirectory2</job-name> <job-group>DEFAULT</job-group> <start-time>2005-06-10 6:10:00 PM</start-time> <!-- repeat indefinitely every 15 seconds --> <repeat-count>-1</repeat-count> <repeat-interval>15000</repeat-interval> </simple> </trigger> Chapter 3, 19 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com </job> </quartz> Running the Quartz scheduler using this modified quartz_jobs.xml file produces the following output: INFO [QuartzScheduler_Worker-0] (ScanDirectoryJob.java:46) - ScanDirectory1 fired at Fri Jun 10 19:22:50 EDT 2005 INFO [QuartzScheduler_Worker-1] (ScanDirectoryJob.java:46) - ScanDirectory1 fired at Fri Jun 10 19:23:00 EDT 2005 INFO [QuartzScheduler_Worker-1] (ScanDirectoryJob.java:46) - ScanDirectory2 fired at Fri Jun 10 19:23:00 EDT 2005 INFO [QuartzScheduler_Worker-2] (ScanDirectoryJob.java:46) - ScanDirectory1 fired at Fri Jun 10 19:23:10 EDT 2005 INFO [QuartzScheduler_Worker-3] (ScanDirectoryJob.java:46) - ScanDirectory2 fired at Fri Jun 10 19:23:15 EDT 2005 INFO [QuartzScheduler_Worker-4] (ScanDirectoryJob.java:46) - ScanDirectory1 fired at Fri Jun 10 19:23:20 EDT 2005 You can see that the ScanDirectory1 job is executed every 10 seconds while ScanDirectory2 does indeed fire every 15 seconds, just as in Listing 3.6 Modifying the quartz.properties File for the Plugin Earlier in this chapter, you were told that the JobInitializationPlugin looks for the file quartz_jobs.xml to get the declarative job information from. If you would like to change that file, you will need to modify the quartz.properties file and tell the plugin which file to load. Listing 3.10 shows how this can be accomplished. We are only repeating the Plugin section here. Listing 3.10 Modifying quartz.properties for JobInitializationPlugin org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.JobInitializationPlugin org.quartz.plugin.jobInitializer.fileName = my_quartz_jobs.xml org.quartz.plugin.jobInitializer.overWriteExistingJobs = true org.quartz.plugin.jobInitializer.validating = false org.quartz.plugin.jobInitializer.overWriteExistingJobs = false org.quartz.plugin.jobInitializer.failOnFileNotFound = true In Listing 3.10, we add the property org.quartz.plugin.jobInitializer.fileName and set the value to the name of our file. It must be available to the classloader, which means somewhere on the classpath. When Quartz starts up and reads the quartz.properties file, it will initialize the plugin. It passes all of these properties to the plugin and the plugin gets notified to look for a different file. Chapter 3, 20 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com Packaging the Quartz Application Let’s finish off this early chapter and briefly discuss the process of packaging an application that utilizes the Quartz framework. Quartz Third-Party Dependencies If you look back at your Quartz distribution, you will see a <quartz_home>\lib directory. In this directory, you’ll find the quartz.jar and several third-party libraries. For deployment, you will absolutely need the quartz.jar as well as some of the other dependent libraries. Which thirdparty libraries are required is somewhat dependent on whether you are running in a standalone environment or as part of a J2EE distribution. Typically the commons libraries (commonslogging, commons-beanutils, etc) are needed regardless. However, when deploying into an application server environment, you need to make sure not to copy JARs that are already present, as you will often get very strange results and we’re not referring to strange as in a “good” way. Table 3.1 lists the included third-part libraries and information to help you decide if you need to include it. Table 3.1 Quartz Third-Party Libraries Required/Optional Name Required Notes and URL Activation.jar http://java.sun.com/products/javabeans/glasgow/jaf.html Commons-beanutils.jar http://jakarta.apache.org/commons/beanutils/ Commons-collections.jar http://jakarta.apache.org/commons/collections/ commons-dbcp-1.1.jar http://jakarta.apache.org/commons/dbcp/ commons-digester.jar If you are using some of the Plugins, you’ll need it commons-logging.jar Yes http://jakarta.apache.org/commons/logging/ commons-pool-1.1.jar http://jakarta.apache.org/commons/pool/ javamail.jar http://java.sun.com/products/javamail/ jdbc2_0-stdext.jar http://java.sun.com/products/jdbc/ jta.jar http://java.sun.com/products/jta/ quartz.jar Yes Core Quartz Framework servlet.jar http://java.sun.com/products/servlet/ Log4j.jar But whose not using this, come on. Configuration and Properties Files It will also be necessary to include the quartz.properties with your application. If you are deploying your application in an exploded format,1 then you should place the An “exploded” format is one that is not contained within a JAR, WAR, EAR or other Java archive, but that is laid on the file system in individual files. 1 Chapter 3, 21 Draft Chapter from upcoming Quartz Book by Chuck Cavaness The track changes feature is turned on so feel free to type your comments, changes or suggestions directly into this document and send the document back to chuckcavaness@yahoo.com quartz.properties file in a directory that is loaded by the classloader. For example, if you have a classes directory, then store the quartz.properties file there. If you have are deploying in one of Java’s Archived formats, then put the properties file in the root of the Archive file. This same rule goes when using the quartz_jobs.xml file as well. Chapter 3, 22