View Javadoc

1   /*
2    * Copyright (c) 2003 Peter Antman, Teknik i Media  <peter.antman@tim.se>
3    *
4    * $Id: CronManagerService.java,v 1.1.1.1 2004/05/19 12:14:31 pra Exp $
5    *
6    * This library is free software; you can redistribute it and/or
7    * modify it under the terms of the GNU Lesser General Public
8    * License as published by the Free Software Foundation; either
9    * version 2 of the License, or (at your option) any later version
10   * 
11   * This library is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14   * Lesser General Public License for more details.
15   * 
16   * You should have received a copy of the GNU Lesser General Public
17   * License along with this library; if not, write to the Free Software
18   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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>  &lt;mbean code="org.backsource.qcron.CronManagerService" name="cron:name=CronManagerService,service=test"&gt;
68      &lt;attribute name="SchedulerInstanceName"&gt;service=test&lt;/attribute&gt;
69      &lt;attribute name="SchedLoggerClass"&gt;org.quartz.impl.Log4jLogger&lt;/attribute&gt;
70      &lt;attribute name="XaTransacted"&gt;false&lt;/attribute&gt;
71      &lt;attribute name="ThreadPoolClass"&gt;org.quartz.simpl.SimpleThreadPool&lt;/attribute&gt;
72      &lt;attribute name="ThreadPoolThreadCount"&gt;3&lt;/attribute&gt;
73      &lt;attribute name="ThreadPoolPrio"&gt;4&lt;/attribute&gt;
74      &lt;attribute name="JobStoreClass"&gt;org.quartz.impl.jdbcjobstore.JobStoreTX&lt;/attribute&gt;
75      &lt;attribute name="JobStoreDriverDelegateClass"&gt;org.quartz.impl.jdbcjobstore.MSSQLDelegate&lt;/attribute&gt;
76       &lt;attribute name="DataSourceDriver"&gt;org.hsqldb.jdbcDriver&lt;/attribute&gt;
77      &lt;attribute name="DataSourceJndiURL"&gt;java:/DefaultDS&lt;/attribute&gt;
78      &lt;attribute name="StorageCreateSqlFile"&gt;hsqldb-sql.xml&lt;/attribute&gt;
79    &lt;/mbean&gt;
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       } // end of if ()
123       
124       return this.name;
125    }
126 
127    //--- Configuration methods for quartz
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       // Fixe properties
310       quartzProp.setProperty(StdSchedulerFactory.PROP_SCHED_RMI_EXPORT,"false");
311       quartzProp.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME,name.toString());
312       //quartzProp.setProperty(StdSchedulerFactory.PROP_JOB_STORE_PREFIX+".instanceId",name.toString());
313 
314       if ( quartzProp.getProperty(PROP_DATASOURCE_DRIVER,null)!=null) {
315          // OK We probably have an SQL JobStore
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       } // end of if ()
322       
323 
324 
325       StdSchedulerFactory factory = new   StdSchedulerFactory();
326       if ( !usePropertyFile) {
327          factory.initialize(quartzProp);
328          
329       } // end of if ()
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       // Set up an SQL connection
349       // for now we really only supports the jndiway.
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       } // end of if ()
354       
355       DataSource ds = (DataSource)new InitialContext().lookup( jndi );
356       Connection conn = ds.getConnection();
357       boolean created = false;
358       ResultSet rs = null;
359       // Check if table exist
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             //if(con != null) try {con.close();con = null;}catch(SQLException e) {}
377          }
378       if ( created) {
379          log.info("Tables for Quertz already created");
380       }else {
381          
382          // Create table
383          try {
384             createTables(conn);
385          }finally {
386             if(conn != null) try {conn.close();conn = null;}catch(SQLException e) {}
387 
388          } // end of finally
389          
390       } // end of if ()
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       //Load create db stuff
399       if ( storageCreateSqlFile == null) {
400          throw new Exception("storageCreateSqlFile is null; can not create tables");
401       } // end of if ()
402 
403       InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(storageCreateSqlFile);
404       if ( is == null) {
405          throw new Exception("Could not locate " + storageCreateSqlFile);
406       } // end of if ()
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          } // end of while ()
421       } catch (Exception e) {
422          conn.rollback();
423          throw e;
424       }
425       
426       conn.commit();
427 
428    }
429 
430 
431    //--- impl of SchedulerFactory
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    //--- impl of CronManager
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             } // end of if ()
458             
459                       
460          } catch (SchedulerException e) {
461             throw new CronException(e);
462          } // end of try-catch
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          } // end of else
474          
475          
476       } catch (SchedulerException e) {
477           throw new CronException(e);
478       } // end of try-catch
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          } // end of if ()
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             //if ( ! scheduler.unscheduleJob( name,group
516             //                            
517             //                            )) {
518             // 
519             //throw new CronException("Could not remove " + group+"."+name);
520             //} 
521          
522          } catch ( SchedulerException e) {
523             throw new CronException(e);
524          } // end of try-catch
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 }// CronManagerService