Saturday, January 31, 2015

Quartz 2 Scheduler Tutorial

Quartz is an open source job scheduling library for Java.

This tutorial will show you how to  implement a job scheduler in your Java enterprise project using Quartz 2.

The Quartz scheduler in this tutorial triggers a job that send out email.

1. Download Quartz

The Quartz library is available for download from the Quartz website.

Put the following files into your EAR lib folder.

  1. c3p0-0.9.1.1
  2. log4j-1.2.16
  3. quartz-2.2.1
  4. quartz-jobs-2.2.1
  5. slf4j-api-1.6.6
  6. slf4j-log4j12-1.6.6


2. Create your quartz properties file 

I named my properties file with the project name, such as quartz_dmowgs.properties.

You may name it as quart.properties but I just prefer including the name of the project to the quartz properties' filename.

Configure the Quartz Scheduler in this properties file.

This properties file should be located in your classpath (eg., your WEB project's src folder). 

Configure the plugin in this file with your job's xml filename eg.; quartz_dmowgs.xml
Declare your job's xml file here.

Here is the quartz_dmowgs.properties file. 

#============================================================================
# Configure Main Scheduler Properties  
#============================================================================

org.quartz.scheduler.instanceName = TestScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.scheduler.threadsInheritContextClassLoaderOfInitializer = true
org.quartz.scheduler.skipUpdateCheck = true

#============================================================================
# Configure ThreadPool  
#============================================================================

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 1
org.quartz.threadPool.threadPriority = 5

#============================================================================
# Configure JobStore  
#============================================================================

org.quartz.jobStore.misfireThreshold = 60000

org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

#============================================================================
# Configure Plugins 
#============================================================================

org.quartz.plugin.triggHistory.class = org.quartz.plugins.history.LoggingJobHistoryPlugin

org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.XMLSchedulingDataProcessorPlugin
org.quartz.plugin.jobInitializer.fileNames = quartz_dmowgs.xml
org.quartz.plugin.jobInitializer.failOnFileNotFound = true
org.quartz.plugin.jobInitializer.scanInterval = 3000
#org.quartz.plugin.jobInitializer.scanInterval = 30
org.quartz.plugin.jobInitializer.wrapInUserTransaction = false

The quartz_dmowgs.xml file contain the xml definition of jobs and associated triggers.
Configure the jobs and triggers that the scheduler will run in this job's xml file.



3.  Declare your QuartzInitializer servlet in the web.xml file.

The QuartzInitializer servlet is used to initialize Quartz, if configured as a load-on-startup servlet in a web application.

Declare your quartz properties (quartz_dmowgs.properties) file as the value for the config-file property of your servlet.

   <servlet> 
    <servlet-name>QuartzInitializer</servlet-name> 
    <servlet-class>org.quartz.ee.servlet.QuartzInitializerServlet</servlet-class> 
    <init-param>
      <param-name>shutdown-on-unload</param-name>
      <param-value>true</param-value>
    </init-param>
    <init-param>
   <param-name>config-file</param-name>
   <param-value>quartz_dmowgs.properties</param-value>
  </init-param>
  <init-param>
   <param-name>start-scheduler-on-load</param-name>
   <param-value>true</param-value>
  </init-param>
  <load-on-startup>3</load-on-startup>
 </servlet>


Declare the other servlets that your job will use.

The EmailReportServlet declared below will be used by the scheduled job to send emails.

 <servlet>
  <description>
  </description>
  <display-name>EmailReportServlet</display-name>
  <servlet-name>EmailReportServlet</servlet-name>
  <servlet-class>com.eric.web.servlet.EmailReportServlet</servlet-class>
 </servlet>



Don't forget the mappings:

 <servlet-mapping>
  <servlet-name>QuartzInitializer</servlet-name>
  <url-pattern>/QuartzInitializer</url-pattern>
 </servlet-mapping> 
 <servlet-mapping>
  <servlet-name>EmailReportServlet</servlet-name>
  <url-pattern>/EmailReportServlet</url-pattern>
 </servlet-mapping>




4. Create your QuartzStrutsPlugin class within a package in your src folder.

A PlugIn is a configuration wrapper for a module-specific resource or service that needs to be notified about application startup and application shutdown events (corresponding to when the container calls init and destroy on the corresponding ActionServlet instance). PlugIn objects can be configured in the struts-config.xml file.

Here is the QuartzStrutsPlugin class.

import javax.servlet.ServletException;

import org.apache.struts.action.ActionServlet;
import org.apache.struts.action.PlugIn;
import org.apache.struts.config.ModuleConfig;
import org.quartz.Scheduler;
import org.quartz.impl.StdSchedulerFactory;



public class QuartzStrutsPlugin implements PlugIn {

 /* (non-Javadoc)
  * @see org.apache.struts.action.PlugIn#destroy()
  */
 public void destroy() {
  // TODO Auto-generated method stub
  
 }

 /* (non-Javadoc)
  * @see org.apache.struts.action.PlugIn#init(org.apache.struts.action.ActionServlet, org.apache.struts.config.ModuleConfig)
  */
 public void init(ActionServlet arg0, ModuleConfig arg1)
   throws ServletException {
  try {
   StdSchedulerFactory sf = new StdSchedulerFactory();

   sf.initialize("quartz_dmowgs.properties");

   Scheduler scheduler = sf.getScheduler();
   scheduler.start();

  } catch (Exception e) {
    System.out.println("Error starting Quartz Scheduler: " + e.getMessage());
    e.printStackTrace();

  }

  
 }

}



5. Declare the QuartzStrutsPlugin in the struts-config.xml file.

 <plug-in className="com.eric.web.plugin.QuartzStrutsPlugin">
 </plug-in>



6.  Create quartz_dmowgs.xml  - the xml file that defines the scheduled job.

The quartz_dmowgs.xml file contain the xml definition of jobs and associated triggers.
Configure the jobs and triggers that the scheduler will run in its xml file.

Here is the quartz_dmowgs.xml file.

<?xml version="1.0" encoding="UTF-8"?>
<job-scheduling-data xmlns="http://www.quartz-scheduler.org/xml/JobSchedulingData"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.quartz-scheduler.org/xml/JobSchedulingData http://www.quartz-scheduler.org/xml/job_scheduling_data_1_8.xsd"
    version="1.8">
    
    <pre-processing-commands>
        <delete-jobs-in-group>*</delete-jobs-in-group>  <!-- clear all jobs in scheduler -->
        <delete-triggers-in-group>*</delete-triggers-in-group> <!-- clear all triggers in scheduler -->
    </pre-processing-commands>
    
    <processing-directives>
        <!-- if there are any jobs/trigger in scheduler of same name (as in this file), overwrite them -->
        <overwrite-existing-data>true</overwrite-existing-data>
        <!-- if there are any jobs/trigger in scheduler of same name (as in this file), and over-write is false, ignore them rather then generating an error -->
        <ignore-duplicates>false</ignore-duplicates> 
    </processing-directives>
    
    <schedule>
     <job>
         <name>EmailReportJob</name>
         <description>Job to generate and email reports</description>
         <job-class>com.eric.wgs.quartz.job.EmailReportJob</job-class>
          <job-data-map>
              <entry>
                  <key>url</key>
                  <value>http://localhost:9084/DMOWGSWeb/EmailReportServlet</value>
              </entry>
              <entry>
                  <key>host</key>
                  <value>localhost</value>
              </entry>              
              <entry>
                  <key>user</key>
                  <value>wasadmin</value>
              </entry>
              <entry>
                  <key>password</key>
                  <value>password123</value>
              </entry>              
       </job-data-map>
     </job>
     <trigger>
           <cron>
             <name>CronTriggerForEmail</name>
             <job-name>EmailReportJob</job-name>
             <start-time>2014-07-31T11:00:00.0</start-time> 
             <end-time>2015-12-31T12:26:00.0</end-time> 
             <misfire-instruction>MISFIRE_INSTRUCTION_SMART_POLICY</misfire-instruction>
             <cron-expression>0 0 8-19 * * ?</cron-expression>
          </cron>
     </trigger>     
    </schedule>    
</job-scheduling-data>




7. Create your job class - EmailReportJob.java

This job will generate and email reports. 

This job generates and email reports by invoking the EmailReportServlet through  the job-data-map property values from the quartz property file quartz_dmowgs.xml.

It uses the JobExecutionContext class of Quartz to get the job-data-map property values.

It also uses the Base64 class to encode the user and password credentials.


Here is the EmailReportJob.java code.


/**
 * @author Eric Soriano
 *
 * This job will generate and email reports
 * 
 */
public class EmailReportJob implements Job {

 private final LogHelper LOG = new LogHelper(this.getClass());

 private URL myURL = null;
 private URLConnection connection = null;

 public void execute(JobExecutionContext context) throws JobExecutionException {
  DefaultHttpClient httpclient = new DefaultHttpClient();
    
  try {   
   JobDataMap data = context.getJobDetail().getJobDataMap();
   String url = data.getString("url"); 
   String host = data.getString("host");
   String user = data.getString("user");
   String pwd = data.getString("password");   
    
   String credentials = user + ":" + pwd;   
   //encode credentials into base 64
   byte[] encoded = Base64.encodeBase64(credentials.getBytes()); 
   String encodedCred = new String(encoded);
  
   //invoke the servlet url (EmailReportServlet - see quartz_dmowgs.xml)
   myURL = new URL(url);
   connection = myURL.openConnection();
   
   //set credentials into URLConnection object
   connection.setRequestProperty("Authorization", String.format("Basic %s", encodedCred)); 
   
   InputStream response = connection.getInputStream();
   
   
  } catch (MalformedURLException mfURLe) {
   LOG.error("execute :: MalformedURLException: " + mfURLe.getMessage());
   mfURLe.printStackTrace();  
  } catch (Exception ex) {
   LOG.error("execute :: HTTP Exception: " + ex.getMessage());
   JobExecutionException e2 = 
          new JobExecutionException(ex);
         throw e2;
  }
 }


 
}


8. Create your servlet - EmailReportServlet.java

This servlet creates an Excel report then uses a EmailSenderService class that uses a EmailDAO class to email the report.

Here is the code for EmailReportServlet.java.


package com.eric.wgs.web.servlet;

public class EmailReportServlet extends HttpServlet  {
 
 private static final long serialVersionUID = 1L;
 private final LogHelper LOG = new LogHelper(this.getClass());
 
 
 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  String method = "doGet()";
  LOG.debug(method, "START");
  
  try {  
   //Create the report (excel file) with the executeTask() method and send as ByteArrayOutputStream object to EmailSenderService
   ByteArrayOutputStream baos = executeTask(request);
   
   //Check if baos is null (intentionally set to null from CompanyProgressReportServiceBean when recipient email list is empty)
   if (baos == null) {
    LOG.debug(method, "ByteArrayOutputStream is null");
    return;
   }
   if(baos != null && baos.size() > 0) {   
    //email the report
    EmailSenderService emailService = new EmailSenderService();
    emailService.sendEmailWithAttachment(baos);    
   } else {
    throw new ApplicationException("executeTask method returned unexpected results: " + baos);
   }   
  } catch(Exception ex) {
   ex.printStackTrace();
   LOG.error("Exception Generating PDF: " + ex.getMessage());
   throw new ServletException(ex);
  }
 } 

 public ByteArrayOutputStream executeTask(HttpServletRequest request) throws IOException, ServletException, ApplicationException {
  String method = "executeTask()";
  LOG.debug(method, "START");
  
  ByteArrayOutputStream buffer = null;
  try {
   ConfigDTO config = new ConfigUtil().getConfig();
   //if ConfigDTO.recipientEmailList is null, return a null ByteArrayOutputStream so report will not be generated and emailed
   if (config.getRecipientEmailList() == null || config.getRecipientEmailList().equals("") ) {
    buffer = null;
    LOG.debug(method, "Recipient email list is null so returning a null baos. Report will not be generated and not emailed.");
    return buffer;    
   }   
      
   //Get the data from the DAO using the service
   CompanyProgressReportServiceBean service = CompanyProgressReportServiceBean.getIntance();
   CompanyProgressReportDTO results = service.getReport();   
      
   //Once I have the results, create workbook using utility
   CompanyProgressReportGeneratorUtil rptUtil = new CompanyProgressReportGeneratorUtil();
   //Workbook wb = rptUtil.createReportExcel(results);
   Workbook wb = rptUtil.createReportExcel(results);   
   
   //Finally add the workbook to the baos to be returned
   buffer = new ByteArrayOutputStream();
   //wb = get me the excel
   wb.write(buffer);
    
  } catch (IOException e) {
   LOG.error(e.getMessage());
   e.printStackTrace();
   throw new ServletException(e.getMessage());
  } catch (Exception e) {
   LOG.error(e.getMessage());
   e.printStackTrace();
   throw new ServletException(e.getMessage());
  }
  LOG.debug(method, "END");
  return buffer;
 } 
 
}