Using Quartz as a cron scheduler for LiveCycle

LCA lot of projects I have worked on have required the ability to schedule a LiveCycle short-lived process to run at a particular time of the day and on particular days. Normally, running a script on any platform is a fairly trivial exercise as you have Task Manager in Windows (ugh) and cron under all Linux and Unix variants.  Being an old SCO OpenServer System V admin, cron is by far my scheduler of choice and people have ported cron over to Windows in binaries such as nnCron and CRONw just to name two. But it still requires you to do something outside of LiveCycle to hook it into your processes. Doing this on Windows can get quite complicated as you have to use batch files to execute PowerShell scripts with security disabled and by the end you just want to have a shower afterwards because its such a hack-a-thon. There had to be a cleaner, more integrated way to do this.

DSC is the key

So I knew LiveCycle ES4 Turnkey runs on JBoss EAP 5.1.0 and that comes with the Quartz Scheduler built in. It dawned on me that I could make use of this from within LiveCycle natively. The key was to use a Component DSC to create your very own scheduler. DSC’s can implement the LifeCycle Interface that contains onStart() and onStop() functions that run whenever the DSC or LiveCycle is started or stopped. So why not use this feature to start and stop your own Quartz Scheduler and plug in whatever Jobs needed doing?

Finding Quartz

First thing I did was locate the version of Quartz that JBoss was running. I scanned the server.log file and found this entry:

2013-08-07 19:20:12,195 INFO [org.quartz.core.QuartzScheduler] (main) Quartz Scheduler v.1.5.2 created.
2013-08-07 19:20:12,204 INFO [org.quartz.simpl.RAMJobStore] (main) RAMJobStore initialized.
2013-08-07 19:20:12,207 INFO [org.quartz.impl.DirectSchedulerFactory] (main) Quartz scheduler 'SimpleQuartzScheduler
2013-08-07 19:20:12,207 INFO [org.quartz.impl.DirectSchedulerFactory] (main) Quartz scheduler version: 1.5.2
2013-08-07 19:20:12,210 INFO [org.quartz.core.QuartzScheduler] (main) Scheduler SimpleQuartzScheduler_$_SIMPLE_NON_CLUSTERED started.

Quartz v1.5.2 was pretty old but would still serve for the purposes I wanted it for. Curiously, the IDPSchedulerService was using v1.6.0 and I dug around for this version, but it seems to be only for use by Adobe’s DSC1, so I left that alone. Also I noticed that deep in the crx-repository there was a Quartz v2.1.1 but I doubted that will be available from within JBoss either.

Creating a test DSC

I started by creating a standard DSC project in Eclipse and included the quartz.jar file I found in C:\Adobe\Adobe LiveCycle ES4\jboss\common\lib (I wont go into how you create a DSC here but this guide is a good place as any to start).

I started by creating a simple CustomSchedulerService class (SchedulerService was already taken by an Adobe DSC) to start and stop the Quartz Scheduler:

import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;

public class CustomSchedulerServiceImpl {

	private static Scheduler scheduler;

	/**
	 * Starts the Quartz Scheduler
	 */
	public static void startScheduler() {
		try {
			SchedulerFactory schedFact = new StdSchedulerFactory();

			scheduler = schedFact.getScheduler();
			scheduler.start();

		} catch (SchedulerException se) {
			se.printStackTrace();
		}
	}

	/**
	 * Stops the Quartz Scheduler
	 */
	public static void stopScheduler() {
		try {
			scheduler.shutdown();
		} catch (SchedulerException se) {
			se.printStackTrace();
		}
	}
}

Then I added the startScheduler() and stopScheduler() methods to LifeCycleImpl.java:

package com.btes.lc.dsc;

import java.util.logging.Level;
import com.adobe.idp.dsc.component.ComponentContext;
import com.adobe.idp.dsc.component.LifeCycle;
import com.adobe.logging.AdobeLogger;
import com.btes.lc.dsc.customschedulerservice.CustomSchedulerServiceImpl;

public class LifeCycleImpl implements LifeCycle {

	private static final AdobeLogger logger = AdobeLogger
			.getAdobeLogger(LifeCycleImpl.class);

	private ComponentContext m_ctx;

	public void setComponentContext(ComponentContext aCtx) {
		if (logger.isLoggable(Level.FINE)) {
			logger.log(Level.FINE, "setComponentContext: "
					+ aCtx.getComponent().getComponentId());
		}
		m_ctx = aCtx;
	}

	public void onStart() {
		logger.log(Level.INFO, "Called onStart: "
				+ m_ctx.getComponent().getComponentId());
		//start up our Quartz scheduler
		CustomSchedulerServiceImpl.startScheduler();
	}

	public void onStop() {
		logger.log(Level.INFO, "Called onStop: "
				+ m_ctx.getComponent().getComponentId());
		//shutdown the scheduler
		CustomSchedulerServiceImpl.stopScheduler();
	}
}

Created a simple component.xml file:

<component xmlns="http://adobe.com/idp/dsc/component/document">
	<component-id>com.btes.lc.dsc.customschedulerservice.CustomSchedulerService</component-id>
	<version>1.0</version>
	<bootstrap-class>com.btes.lc.dsc.BootstrapImpl</bootstrap-class>
	<lifecycle-class>com.btes.lc.dsc.LifeCycleImpl</lifecycle-class>
	<services>
		<service name="CustomSchedulerService" orchestrateable="true" title="CustomSchedulerService">
			<implementation-class>com.btes.lc.dsc.customschedulerservice.CustomSchedulerServiceImpl</implementation-class>
			<auto-deploy major-version="1" minor-version="0" category-id="CustomScheduler" />
			<operations>
			</operations>
		</service>
	</services>
	<supports-export>true</supports-export>
   <class-path>quartz.jar</class-path>
</component>

I then did a quick build and installed and started the Component into LiveCycle via Workbench and voila!

2013-08-07 22:22:56,610 INFO  [com.btes.lc.dsc.LifeCycleImpl] (http-0.0.0.0-8080-5) Called onStart: com.btes.lc.dsc.customschedulerservice.CustomSchedulerService
2013-08-07 22:22:56,616 INFO  [org.quartz.simpl.SimpleThreadPool] (http-0.0.0.0-8080-5) Job execution threads will use class loader of thread: http-0.0.0.0-8080-5
2013-08-07 22:22:56,618 INFO  [org.quartz.core.QuartzScheduler] (http-0.0.0.0-8080-5) Quartz Scheduler v.1.5.2 created.
2013-08-07 22:22:56,618 INFO  [org.quartz.simpl.RAMJobStore] (http-0.0.0.0-8080-5) RAMJobStore initialized.
2013-08-07 22:22:56,618 INFO  [org.quartz.impl.StdSchedulerFactory] (http-0.0.0.0-8080-5) Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties'
2013-08-07 22:22:56,618 INFO  [org.quartz.impl.StdSchedulerFactory] (http-0.0.0.0-8080-5) Quartz scheduler version: 1.5.2
2013-08-07 22:22:56,618 INFO  [org.quartz.core.QuartzScheduler] (http-0.0.0.0-8080-5) Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.

Stopping the DSC also stopped the scheduler:

2013-08-07 22:27:03,949 INFO  [com.adobe.idp.dsc.registry.service.impl.ServiceRegistryImpl] (http-0.0.0.0-8080-5) ServiceRegistryImpl:stop(CustomSchedulerService, 1.0)
2013-08-07 22:27:03,982 INFO  [com.btes.lc.dsc.LifeCycleImpl] (http-0.0.0.0-8080-5) Called onStop: com.btes.lc.dsc.customschedulerservice.CustomSchedulerService
2013-08-07 22:27:03,982 INFO  [org.quartz.core.QuartzScheduler] (http-0.0.0.0-8080-5) Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED shutting down.
2013-08-07 22:27:03,982 INFO  [org.quartz.core.QuartzScheduler] (http-0.0.0.0-8080-5) Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED paused.
2013-08-07 22:27:03,982 INFO  [org.quartz.core.QuartzScheduler] (http-0.0.0.0-8080-5) Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED shutdown complete.

I am assuming that DefaultQuartzScheduler is the name assigned inside the default quartz.properties configuration file that is inside the quartz.jar file provided by JBoss.

Do something

Next I created a simple Job file that I would eventually use to invoke a process. For now it will just print some output into the server.log at regular intervals.

package com.btes.lc.dsc.customschedulerservice.jobs;
import java.util.Date;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
/**
 * Simple Process Job for Livecycle
 *
 */
public class ProcessJob implements Job {

	@Override
	public void execute(JobExecutionContext context) throws JobExecutionException {
		String jobKey = context.getJobDetail().getName();
		String date = new Date().toString();
		System.out.println("ProcessJob says: " + jobKey + " executing at " + date);
	}
}

Then I would need a Trigger to start the Job which I put into the main Component code for now and scheduled the job as soon as the Scheduler was started. The cron expression "0/20 * * * * ?" would execute the CronTrigger every 20 seconds until the scheduler was stopped.

package com.btes.lc.dsc.customschedulerservice;

import java.text.ParseException;
import org.quartz.CronTrigger;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;
import com.btes.lc.dsc.customschedulerservice.jobs.ProcessJob;

public class CustomSchedulerServiceImpl implements CustomSchedulerService {

	private static Scheduler scheduler;

	/**
	 * Starts the Quartz Scheduler
	 */
	public static void startScheduler() {
		try {
			SchedulerFactory schedFact = new StdSchedulerFactory();

			scheduler = schedFact.getScheduler();
			scheduler.start();

			CustomSchedulerServiceImpl.scheduleJobs();

		} catch (SchedulerException se) {
			se.printStackTrace();
		}
	}

	/**
	 * Stops the Quartz Scheduler
	 */
	public static void stopScheduler() {
		try {
			scheduler.shutdown();
		} catch (SchedulerException se) {
			se.printStackTrace();
		}
	}

	/**
	 * Schedules some jobs
	 */
	public static void scheduleJobs() {
		try {
			JobDetail job = new JobDetail("myJob", Scheduler.DEFAULT_GROUP, ProcessJob.class);
			CronTrigger trigger = new CronTrigger();
			trigger.setCronExpression("0/20 * * * * ?");
			trigger.setGroup(Scheduler.DEFAULT_GROUP);
			trigger.setName("Trigger1");

			scheduler.scheduleJob(job, trigger);

		} catch (SchedulerException se) {
			se.printStackTrace();
		} catch (ParseException e) {
			e.printStackTrace();
		}
	}
}

Compiling this code and deploying it to the LiveCycle server gave this output to STDOUT:

2013-08-07 22:45:00,017 INFO  [STDOUT] (DefaultQuartzScheduler_Worker-7) ProcessJob says: myJob executing at Wed Aug 07 22:45:00 EST 2013
2013-08-07 22:45:20,019 INFO  [STDOUT] (DefaultQuartzScheduler_Worker-8) ProcessJob says: myJob executing at Wed Aug 07 22:45:20 EST 2013

Now to make it really DoSomething

Next, I created a very simple application within Workbench that would accept some input and write that out to the server.log file. Crucially, this input should come from the Quartz Scheduler.

SchedulerTest Application

SchedulerTest Application

The executeScript process just grabs the two incoming parameters (inJobName and inDate) and writes them to STDOUT in the server.log file.

System.out.println("SchedulerTest::DoSomething - I am doing something with: "
+ patExecContext.getProcessDataStringValue("/process_data/@inJobName")
+ " invoked on " + patExecContext.getProcessDataStringValue("/process_data/@inDate"));

Next I added the relevant libraries to my Eclipse project and the code to invoke the LiveCycle process to the Job:

package com.btes.lc.dsc.customschedulerservice.jobs;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import com.adobe.idp.dsc.DSCException;
import com.adobe.idp.dsc.InvocationRequest;
import com.adobe.idp.dsc.InvocationResponse;
import com.adobe.idp.dsc.clientsdk.ServiceClient;
import com.adobe.idp.dsc.clientsdk.ServiceClientFactory;
import com.adobe.idp.dsc.clientsdk.ServiceClientFactoryProperties;

/**
 * Simple Process Job for Livecycle
 *
 */
public class ProcessJob implements Job {

	@Override
	public void execute(JobExecutionContext context)
			throws JobExecutionException {
		String jobKey = context.getJobDetail().getName();
		String date = new Date().toString();
		System.out.println("ProcessJob says: " + jobKey + " executing at " + date);

		Properties ConnectionProps = new Properties();
		ConnectionProps.setProperty(
				ServiceClientFactoryProperties.DSC_DEFAULT_EJB_ENDPOINT,
				"jnp://localhost:1099");
		ConnectionProps.setProperty(
				ServiceClientFactoryProperties.DSC_TRANSPORT_PROTOCOL,
				ServiceClientFactoryProperties.DSC_EJB_PROTOCOL);
		ConnectionProps.setProperty(
				ServiceClientFactoryProperties.DSC_SERVER_TYPE, "JBoss");
		ConnectionProps.setProperty(
				ServiceClientFactoryProperties.DSC_CREDENTIAL_USERNAME,
				"administrator");
		ConnectionProps.setProperty(
				ServiceClientFactoryProperties.DSC_CREDENTIAL_PASSWORD,
				"password");

		ServiceClientFactory myFactory = ServiceClientFactory
				.createInstance(ConnectionProps);

		ServiceClient myServiceClient = myFactory.getServiceClient();

		// Create a Map object to store the parameter value

		Map params = new HashMap();

		// Populate the Map object with a parameter value
		params.put("inJobName", context.getJobDetail().getName());
		params.put("inDate", date);

		// Create an InvocationRequest object
		InvocationRequest esRequest = myFactory.createInvocationRequest(
				"SchedulerTest/DoSomething", // Specify the LiveCycle ES process name
				"invoke", // Specify the operation name
				params, // Specify input values
				true); // Create a synchronous request

		// Send the invocation request to the process and
		// get back an invocation response object
		try {
			InvocationResponse esResponse = myServiceClient.invoke(esRequest);
		} catch (DSCException e) {
			e.printStackTrace();
		}
	}
}

I deployed both the Application and the DSC to LiveCycle. I started the DSC and got this as the output:

2013-08-07 23:06:00,020 INFO  [STDOUT] (DefaultQuartzScheduler_Worker-9) ProcessJob says: myJob executing at Wed Aug 07 23:06:00 EST 2013
2013-08-07 23:06:00,060 INFO  [STDOUT] (DefaultQuartzScheduler_Worker-9) SchedulerTest::DoSomething - I am doing something with: myJob invoked on Wed Aug 07 23:06:00 EST 2013
2013-08-07 23:06:20,015 INFO  [STDOUT] (DefaultQuartzScheduler_Worker-9) ProcessJob says: myJob executing at Wed Aug 07 23:06:20 EST 2013
2013-08-07 23:06:20,047 INFO  [STDOUT] (DefaultQuartzScheduler_Worker-9) SchedulerTest::DoSomething - I am doing something with: myJob invoked on Wed Aug 07 23:06:20 EST 2013

As you can see, the CronTrigger ran the ProcessJob every 20 seconds which in turn invoked the LiveCycle process. I’m happy!

Where to from here?

This is obviously a very basic proof-of-concept to show that invoking a LiveCycle process using cron expressions is possible natively from within Livecycle on JBoss. There are a number of things you would need to do to harden this up like use your own quartz.properties file as well as extend it for all sorts of custom configuration. There are plugins for Quartz that allow you to create an XML file to define your jobs and pass in all of the required information to start, stop and handle the execution of those jobs properly. It wouldn’t be a complex exercise to set this up.

Sample files

Below is the sample LCA and the DSC source that I created for this exercise. The libraries are for ES3/ADEP and ES4 but if you replace the libs with ES2.5 libraries, the code should also work.

CustomSchedulerService.zip

SchedulerTest.lca

1 I dug around some more and the IDPSchedulerService does indeed bundle a version of v1.6.0 of quartz.jar inside the adobe-scheduler-jboss.jar which makes me wonder if its possible to replace the v1.5.2 version in JBoss with a later v1.8.5 without breaking anything LiveCycle related…

Advertisements

About Darren
I am a Senior Consultant and for BizTECH Enterprise Services (Australia) specialising in AEM Forms and Adobe LiveCycle, Document Services and Adobe Experience Manager.

6 Responses to Using Quartz as a cron scheduler for LiveCycle

  1. Don Andrews says:

    Darren, this is brilliant! Just exactly what I’m trying to do and all in one place. Thanks!

    • Don Andrews says:

      Btw, you don’t need any of the jar files in the component jar nor do you need the classpath element in the component.xml. I replaced the classpath element with the dynamic-import-packages section below and it works just fine. Reminder to first-time component developers: deployment is via Workbench (Window -> Show View -> Components).

      com.adobe.idp.dsc.component
      com.adobe.logging
      org.quartz.*

      • Darren says:

        Thanks for that Don. Its been a while since I looked at it, but if I get a chance I will update it.

      • Don Andrews says:

        Have you figured out how to get the job started when the server re-starts? In my system it doesn’t seem to do that.

      • Darren says:

        I assumed that the onStart() function inside the DSC would just fire when the DSC is reloaded during server restart and would automatically start the scheduler again.

        Do you get any errors during reboots? Is the service started when you view it in Workbench?

      • Don Andrews says:

        You’re right, the component as-written uses the Adobe-defined JobStore which persists across system starts. I finally figured that out when I wrote a small servlet that queries the Quartz job store and discovered that my job was already there. My previous use of Quartz was with RAMJobStore (Quartz 1.6 I think) and I managed the jobs.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

aemblog

Everything AEM aka CQ5 based on my experience listed here.

Adobe AEM The Right Way

Best practices, tips, and tricks for your Adobe AEM project

/home/bkondepudi

A WCM journey with Day/Adobe CQ

Technoracle (a.k.a. "Duane's World")

A multi-purpose toolkit for the Adobe LiveCycle and AEM Forms developer.

Adobe LiveCycle Blog

A multi-purpose toolkit for the Adobe LiveCycle and AEM Forms developer.

A multi-purpose toolkit for the Adobe LiveCycle and AEM Forms developer.

XFA@Mobile

A multi-purpose toolkit for the Adobe LiveCycle and AEM Forms developer.

Code Monkey

Ramblings of a Developer

%d bloggers like this: