1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package org.backsource.qcron;
21 import java.lang.reflect.Method;
22 import java.lang.reflect.Proxy;
23 import java.lang.reflect.InvocationHandler;
24 import java.io.InputStream;
25 import java.util.Properties;
26 import java.util.Iterator;
27 import java.util.List;
28 import java.util.Arrays;
29 import java.util.ArrayList;
30 import java.sql.Connection;
31 import java.sql.Statement;
32 import java.sql.ResultSet;
33 import java.sql.SQLException;
34 import java.sql.DatabaseMetaData;
35
36 import javax.sql.DataSource;
37 import javax.naming.InitialContext;
38 import javax.management.ObjectName;
39 import javax.management.MBeanServer;
40 import javax.management.MalformedObjectNameException;
41
42 import org.w3c.dom.Document;
43 import org.w3c.dom.Element;
44
45 import org.quartz.SchedulerFactory;
46 import org.quartz.Scheduler;
47 import org.quartz.JobDetail;
48 import org.quartz.Trigger;
49 import org.quartz.CronTrigger;
50 import org.quartz.SchedulerException;
51 import org.quartz.impl.StdSchedulerFactory;
52
53 import org.backsource.utils.xml.DocumentUtil;
54 import org.backsource.utils.xml.ElementUtil;
55
56 import org.backsource.jmx.ServiceMBeanSupport;
57 import org.backsource.jmx.ObjectNameFactory;
58 /***
59 * An MBean service of CronManager based on the Quartz scheduler.
60 *
61 * <p>This is an MBean wrapper on top of Quartz. It might be seen as a jboss-service configurable Quartz SchedulerFactory. Its is however also a {@link CronManager},which gives a somewhat simpler API to place jobbs to be done int Quartz, and also makes it easier to call object and othet MBeans.</p>
62 * <p>One thing is inportant to know: quartz does not seem to handle volatile
63 jobbs correct when run with a JDBC store. To use volatile jobs one has to
64 use an instance of this service wich ise configured with a org.quartz.simpl.RAMJobStore JobStoreClass.</p>
65 *
66 * <p>Through this MBean Quartz is fully configurable throug an jboss-service.xml file; no Quartz property file is needed.There are a lot of options here is one example on an mbean stanza that uses the Hypersonic database as its persistant storage:</p>
67 <pre> <mbean code="org.backsource.qcron.CronManagerService" name="cron:name=CronManagerService,service=test">
68 <attribute name="SchedulerInstanceName">service=test</attribute>
69 <attribute name="SchedLoggerClass">org.quartz.impl.Log4jLogger</attribute>
70 <attribute name="XaTransacted">false</attribute>
71 <attribute name="ThreadPoolClass">org.quartz.simpl.SimpleThreadPool</attribute>
72 <attribute name="ThreadPoolThreadCount">3</attribute>
73 <attribute name="ThreadPoolPrio">4</attribute>
74 <attribute name="JobStoreClass">org.quartz.impl.jdbcjobstore.JobStoreTX</attribute>
75 <attribute name="JobStoreDriverDelegateClass">org.quartz.impl.jdbcjobstore.MSSQLDelegate</attribute>
76 <attribute name="DataSourceDriver">org.hsqldb.jdbcDriver</attribute>
77 <attribute name="DataSourceJndiURL">java:/DefaultDS</attribute>
78 <attribute name="StorageCreateSqlFile">hsqldb-sql.xml</attribute>
79 </mbean>
80 </pre>
81 *
82 * @author <a href="mailto:pra@tim.se">Peter Antman</a>
83 * @version $Revision: 1.1.1.1 $
84 * @jmx:mbean name="cron:name=CronManager" extends="org.backsource.qcron.CronManager,org.quartz.SchedulerFactory,org.backsource.jmx.ServiceMBean"
85 */
86
87 public class CronManagerService
88 extends ServiceMBeanSupport
89 implements CronManagerServiceMBean,SchedulerFactory {
90
91 public static final String DATASOURCE_NAME = "cmsDs";
92 public static final String PROP_DATASOURCE_PREFIX = StdSchedulerFactory.PROP_DATASOURCE_PREFIX +"." + DATASOURCE_NAME;
93 public static final String PROP_DATASOURCE_DRIVER = PROP_DATASOURCE_PREFIX+"."+StdSchedulerFactory.PROP_DATASOURCE_DRIVER;
94 public static final String PROP_DATASOURCE_URL =PROP_DATASOURCE_PREFIX+"."+StdSchedulerFactory.PROP_DATASOURCE_URL;
95 public static final String PROP_DATASOURCE_USER =PROP_DATASOURCE_PREFIX+"."+StdSchedulerFactory.PROP_DATASOURCE_USER;
96 public static final String PROP_DATASOURCE_PASSWORD =PROP_DATASOURCE_PREFIX+"."+StdSchedulerFactory.PROP_DATASOURCE_PASSWORD;
97 public static final String PROP_DATASOURCE_MAX_CONNECTIONS =PROP_DATASOURCE_PREFIX+"."+StdSchedulerFactory.PROP_DATASOURCE_MAX_CONNECTIONS;
98 public static final String PROP_DATASOURCE_JNDI_URL =PROP_DATASOURCE_PREFIX+"."+StdSchedulerFactory.PROP_DATASOURCE_JNDI_URL;
99 public static final String PROP_DATASOURCE_VALIDATION_QUERY =PROP_DATASOURCE_PREFIX+"."+StdSchedulerFactory.PROP_DATASOURCE_VALIDATION_QUERY;
100 public static final String TABLE_PREFIX = "QRTZ_";
101 public static final String TABLE_CHECK = TABLE_PREFIX+"job_details";
102
103 public static final javax.management.ObjectName OBJECT_NAME = ObjectNameFactory.create("cron:name=CronManager");
104 protected ObjectName name = OBJECT_NAME;
105 protected MBeanServer server;
106
107 protected Properties quartzProp = new Properties();
108 protected boolean usePropertyFile = false;
109 protected String storageCreateSqlFile;
110 private Scheduler scheduler;
111 private Scheduler schedDelegate;
112
113 public CronManagerService (){
114
115 }
116 protected ObjectName getObjectName(MBeanServer server, ObjectName name)
117 throws MalformedObjectNameException
118 {
119 this.server = server;
120 if ( name != null) {
121 this.name = name;
122 }
123
124 return this.name;
125 }
126
127
128 /***
129 * A uniqe name for this Quartz instance.
130 * @jmx:managed-attribute
131 */
132 public void setSchedulerInstanceName(String name) {
133 quartzProp.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME,
134 name);
135 }
136
137 /***
138 * The logger class to use; the same for all components.
139 * @jmx:managed-attribute
140 */
141 public void setSchedLoggerClass(String name) {
142 initLoggers(name);
143
144 }
145
146 private void initLoggers(String name) {
147 quartzProp.setProperty(StdSchedulerFactory.PROP_SCHED_LOGGER,
148 "schedLogger");
149 quartzProp.setProperty(StdSchedulerFactory.PROP_LOGGER_PREFIX+".schedLogger.class",
150 name);
151 quartzProp.setProperty(StdSchedulerFactory.PROP_LOGGER_PREFIX+".schedLogger.categoryName",
152 "scheduler");
153
154 quartzProp.setProperty(StdSchedulerFactory.PROP_THREAD_POOL_LOGGER,
155 "tpLogger");
156 quartzProp.setProperty(StdSchedulerFactory.PROP_LOGGER_PREFIX+".tpLogger.class",
157 name);
158 quartzProp.setProperty(StdSchedulerFactory.PROP_LOGGER_PREFIX+".tpLogger.categoryName",
159 "scheduler.threadPool");
160
161 quartzProp.setProperty(StdSchedulerFactory.PROP_JOB_STORE_LOGGER,
162 "jsLogger");
163 quartzProp.setProperty(StdSchedulerFactory.PROP_LOGGER_PREFIX+".jsLogger.class",
164 name);
165 quartzProp.setProperty(StdSchedulerFactory.PROP_LOGGER_PREFIX+".jsLogger.categoryName",
166 "scheduler.persistence");
167 }
168 /***
169 * Set if Quartz should use XA transactio: does currently not work since Quartz expect user transaction to be in jndi: java:comp/UserTransaction.
170 * @jmx:managed-attribute
171 */
172 public void setXaTransacted(Boolean bool) {
173 quartzProp.setProperty(StdSchedulerFactory.PROP_SCHED_XA_TRANSACTED,bool.toString());
174 }
175 /***
176 * Thread pool class to use in Quartz.
177 * @jmx:managed-attribute
178 */
179 public void setThreadPoolClass(String name) {
180 quartzProp.setProperty(StdSchedulerFactory.PROP_THREAD_POOL_CLASS,name);
181 }
182 /***
183 * Number of Threads.
184 * @jmx:managed-attribute
185 */
186 public void setThreadPoolThreadCount(Integer name) {
187 quartzProp.setProperty(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX+".threadCount",name.toString());
188 }
189 /***
190 * No idea.
191 * @jmx:managed-attribute
192 */
193 public void setThreadPoolPrio(Integer name) {
194 quartzProp.setProperty(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX+".threadPriority",name.toString());
195 }
196 /***
197 * Class to use for job store.
198 * @jmx:managed-attribute
199 */
200 public void setJobStoreClass(String name) {
201 quartzProp.setProperty(StdSchedulerFactory.PROP_JOB_STORE_CLASS,name);
202 }
203 /***
204 * Driver delegate if using jdbc.
205 * @jmx:managed-attribute
206 */
207 public void setJobStoreDriverDelegateClass(String name) {
208 quartzProp.setProperty(StdSchedulerFactory.PROP_JOB_STORE_PREFIX+".driverDelegateClass",name);
209 }
210
211 /***
212 * JDBC driver if using jdbc.
213 * @jmx:managed-attribute
214 */
215 public void setDataSourceDriver(String name) {
216 quartzProp.setProperty(PROP_DATASOURCE_DRIVER,name);
217 }
218 /***
219 * JDBC URL if not using tDataSourceJndiURL.
220 * @jmx:managed-attribute
221 */
222 public void setDataSourceURL(String name) {
223 quartzProp.setProperty(PROP_DATASOURCE_URL,name);
224 }
225 /***
226 * JDBC user if not using tDataSourceJndiURL.
227 * @jmx:managed-attribute
228 */
229 public void setDataSourceUser(String name) {
230 quartzProp.setProperty(PROP_DATASOURCE_USER,name);
231 }
232 /***
233 * JDBC password if not using tDataSourceJndiURL.
234 * @jmx:managed-attribute
235 */
236 public void setDataSourcePassword(String name) {
237 quartzProp.setProperty(PROP_DATASOURCE_PASSWORD,name);
238 }
239 /***
240 * Max open connections if not using tDataSourceJndiURL.
241 * @jmx:managed-attribute
242 */
243 public void setDataSourceMaxCon(Integer name) {
244 quartzProp.setProperty(PROP_DATASOURCE_MAX_CONNECTIONS,name.toString());
245 }
246 /***
247 * The JNDI url to a DataSource, J2EE fashion, currently the only supported way!
248 * @jmx:managed-attribute
249 */
250 public void setDataSourceJndiURL(String name) {
251 quartzProp.setProperty(PROP_DATASOURCE_JNDI_URL,name);
252 }
253 /***
254 * ?
255 * @jmx:managed-attribute
256 */
257 public void setDataSourceValidationQuery(String name) {
258 quartzProp.setProperty(PROP_DATASOURCE_VALIDATION_QUERY,name);
259 }
260 /***
261 * Probably not used any more!
262 * @jmx:managed-attribute
263 */
264 public void setSqlServerStyleBlob(Boolean use) {
265 quartzProp.setProperty(StdSchedulerFactory.PROP_JOB_STORE_PREFIX+".sqlServerStyleBlobs",use.toString());
266 }
267 /***
268 * Probably not used any more!
269 * @jmx:managed-attribute
270 */
271 public void setPostgresStyleBlob(Boolean use) {
272 quartzProp.setProperty(StdSchedulerFactory.PROP_JOB_STORE_PREFIX+".postgresStyleBlobs",use.toString());
273 }
274 /***
275 * Get all properties Quartz was inited with.
276 * @jmx:managed-attribute
277 */
278 public Properties getProperties() {
279 return quartzProp;
280 }
281 /***
282 * Use property file instead of the Mbeans attributes.
283 * @jmx:managed-attribute
284 */
285 public void setUsePropertyFile(boolean use) {
286 this.usePropertyFile = use;
287 }
288 /***
289 * @jmx:managed-attribute
290 */
291 public boolean getUsePropertyFile() {
292 return usePropertyFile;
293 }
294 /***
295 * The name of an XML file containg SQL create table instruction to autmaticllat create the db tables; must be for the correct db and be part of the context class loader path.
296 * @jmx:managed-attribute
297 */
298 public void setStorageCreateSqlFile(String storageCreateSqlFile) {
299 this.storageCreateSqlFile = storageCreateSqlFile;
300 }
301 /***
302 * @jmx:managed-attribute
303 */
304 public String getStorageCreateSqlFile() {
305 return storageCreateSqlFile;
306 }
307
308 protected void startService() throws Exception {
309
310 quartzProp.setProperty(StdSchedulerFactory.PROP_SCHED_RMI_EXPORT,"false");
311 quartzProp.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME,name.toString());
312
313
314 if ( quartzProp.getProperty(PROP_DATASOURCE_DRIVER,null)!=null) {
315
316 quartzProp.setProperty(StdSchedulerFactory.PROP_JOB_STORE_PREFIX+".dataSource",DATASOURCE_NAME);
317 quartzProp.setProperty(StdSchedulerFactory.PROP_JOB_STORE_PREFIX+".tablePrefix",TABLE_PREFIX);
318 quartzProp.setProperty(StdSchedulerFactory.PROP_JOB_STORE_PREFIX+".instanceId", quartzProp.getProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME));
319 setUpJobbStore();
320
321 }
322
323
324
325 StdSchedulerFactory factory = new StdSchedulerFactory();
326 if ( !usePropertyFile) {
327 factory.initialize(quartzProp);
328
329 }
330 scheduler = factory.getScheduler();
331 schedDelegate = (Scheduler)Proxy.newProxyInstance(
332 Thread.currentThread().getContextClassLoader(),
333 new Class[] {Scheduler.class},
334 new SyncSched(scheduler)
335 );
336 scheduler.start();
337 }
338
339 protected void stopService() throws Exception {
340 scheduler.shutdown();
341 }
342
343 /***
344 * Set up the jobstorage, currently only if a jndi url was given.
345 *<p>if the db tables is not found they will be automatically created.
346 */
347 protected void setUpJobbStore() throws Exception {
348
349
350 String jndi = quartzProp.getProperty(PROP_DATASOURCE_JNDI_URL,null);
351 if ( jndi == null) {
352 throw new Exception("We only handle jndi for datasource");
353 }
354
355 DataSource ds = (DataSource)new InitialContext().lookup( jndi );
356 Connection conn = ds.getConnection();
357 boolean created = false;
358 ResultSet rs = null;
359
360 try
361 {
362 DatabaseMetaData dmd = conn.getMetaData();
363 rs = dmd.getTables(conn.getCatalog(), null, TABLE_CHECK, null);
364 if (rs.next ())
365 created = true;
366
367 rs.close ();
368 }
369 catch(Exception e)
370 {
371 throw e;
372 }
373 finally
374 {
375 if(rs != null) try {rs.close(); rs = null;}catch(SQLException e) {}
376
377 }
378 if ( created) {
379 log.info("Tables for Quertz already created");
380 }else {
381
382
383 try {
384 createTables(conn);
385 }finally {
386 if(conn != null) try {conn.close();conn = null;}catch(SQLException e) {}
387
388 }
389
390 }
391
392 }
393 /***
394 * Create the Quartz db tables.
395 */
396 protected void createTables(Connection conn) throws Exception {
397 log.info("Creating quertz tables");
398
399 if ( storageCreateSqlFile == null) {
400 throw new Exception("storageCreateSqlFile is null; can not create tables");
401 }
402
403 InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(storageCreateSqlFile);
404 if ( is == null) {
405 throw new Exception("Could not locate " + storageCreateSqlFile);
406 }
407
408 Document doc = DocumentUtil.getDocument(is);
409 Iterator sqlNodes = ElementUtil.getElementsByTagName(doc.getDocumentElement(),
410 "create-sql").iterator();
411 conn.setAutoCommit(false);
412 try {
413 while ( sqlNodes.hasNext()) {
414 Element sqlNode = (Element)sqlNodes.next();
415 String sql = ElementUtil.getContent(sqlNode);
416 log.debug("Creating table with " + sql);
417 Statement stm = conn.createStatement();
418 int rows = stm.executeUpdate(sql);
419
420 }
421 } catch (Exception e) {
422 conn.rollback();
423 throw e;
424 }
425
426 conn.commit();
427
428 }
429
430
431
432 /***
433 * Return a synchronized Quartz scheduler.
434 *
435 * @jmx:managed-operation
436 */
437 public Scheduler getScheduler()
438 throws SchedulerException {
439 return schedDelegate;
440 }
441
442
443 /***
444 * @jmx:managed-operation
445 */
446 public void addCronEntry(CronEntry entry) throws CronException {
447 log.debug("Adding cronEntry: " +entry.getJobDetail().getFullName());
448 synchronized(scheduler) {
449 try {
450
451 CronTrigger t = entry.getTrigger();
452 scheduler.scheduleJob(entry.getJobDetail(),
453 t
454 );
455 if ( log.isDebugEnabled()) {
456 debugTrigger(t);
457 }
458
459
460 } catch (SchedulerException e) {
461 throw new CronException(e);
462 }
463 }
464 }
465
466 public List getCronEntryNames(String group) throws CronException {
467 try {
468 String[] names = scheduler.getJobNames(group);
469 if ( name != null) {
470 return Arrays.asList(names);
471 } else {
472 return new ArrayList();
473 }
474
475
476 } catch (SchedulerException e) {
477 throw new CronException(e);
478 }
479
480
481 }
482 public CronEntry getCronEntry(String group, String name) throws CronException{
483 log.debug("Getting cronEntry: " + group+"."+name);
484 CronEntry entry = null;
485 try {
486 JobDetail job = scheduler.getJobDetail(name,group);
487 Trigger trigger = scheduler.getTrigger(name,group);
488 if ( job != null) {
489 entry = new CronEntry((CronTrigger)trigger,job);
490 }
491
492 } catch (SchedulerException e) {
493 throw new CronException(e);
494 }
495 return entry;
496 }
497 /***
498 * @jmx:managed-operation
499 */
500 public void removeCronEntry(CronEntry entry) throws CronException {
501 removeCronEntry(entry.getEntryGroup(),entry.getEntryName());
502 }
503 /***
504 * @jmx:managed-operation
505 */
506 public void removeCronEntry(String group, String name) throws CronException {
507 log.debug("Removing entry: "+group+"."+name);
508 try {
509
510 if ( ! scheduler.unscheduleJob( name,group
511
512 )) {
513 log.error("Could perhaps not remove " + group+"."+name);
514 }
515
516
517
518
519
520
521
522 } catch ( SchedulerException e) {
523 throw new CronException(e);
524 }
525 }
526 class SyncSched implements InvocationHandler {
527 Scheduler delegate;
528 protected SyncSched(Scheduler delegate) {
529 this.delegate = delegate;
530 }
531 public Object invoke(Object proxy,
532 Method method,
533 Object[] args)
534 throws Throwable {
535 synchronized(delegate) {
536 return method.invoke(delegate,args);
537 }
538 }
539
540 }
541
542 private void debugTrigger(CronTrigger trigger) {
543 log.debug("Trigger exp:"+trigger.getExpressionSummary());
544 log.debug("Trigger will fire next time:"+trigger.getNextFireTime());
545 log.debug("Trigger: "+trigger+" Job="+trigger.getJobGroup()+"."+trigger.getJobName());
546
547 }
548 }