001: package de.webman.util.scheduler;
002:
003: import de.webman.util.registry.Manager;
004: import de.webman.util.registry.RegistryException;
005: import de.webman.util.log4j.WebmanCategory;
006: import org.apache.log4j.Category;
007: import java.io.File;
008: import java.io.IOException;
009: import java.io.BufferedInputStream;
010: import java.io.FileInputStream;
011: import java.util.List;
012: import java.util.Iterator;
013: import java.util.Map;
014: import java.util.HashMap;
015: import java.util.ArrayList;
016: import org.w3c.dom.Node;
017: import org.w3c.dom.Document;
018: import org.w3c.dom.Element;
019: import org.xml.sax.SAXException;
020: import org.xml.sax.InputSource;
021: import javax.xml.parsers.DocumentBuilderFactory;
022: import javax.xml.parsers.DocumentBuilder;
023: import javax.xml.parsers.ParserConfigurationException;
024: import java.text.SimpleDateFormat;
025: import java.text.ParseException;
026: import java.util.GregorianCalendar;
027: import java.util.Calendar;
028: import java.util.Date;
029:
030: /**
031: * The central scheduler manager, implementing the {@link
032: * de.webman.util.Manager} protocol.<p>
033: *
034: * How to use ** TODO **<p>
035: *
036: * Format of the xml-config file:<p>
037: *
038: * <code><pre>
039: * <scheduler>
040: * <service id="gc-run"
041: * factory-class="de.webman.util.gc.GGSchedulerFactory"
042: * frequency="once"
043: * at-date="12.05.2002" at-time="20:10:45">
044: * <param key="init-file" value="gc.xml" relative="true"/>
045: * </service>
046: * <service id="sync-run"
047: * factory-class="de.webman.sync.SyncSchedulerFactory"
048: * frequency="15"
049: * freq-unit="minutes" />
050: * </scheduler>
051: * </pre></code>
052: *
053: * <code><pre>
054: * <!ELEMENT scheduler (service*)>
055: * <!ELEMENT service (param*)>
056: * <!ATTLIST service id CDATA #IMPLIED
057: * factory-class CDATA #REQUIRED
058: * frequency CDATA #REQUIRED -- may take "once" or any int number --
059: * freq-unit CDATA #IMPLIED -- "seconds|minutes|hours|days", defaults to minutes --
060: * delay CDATA #IMPLIED -- may take any int number, defaults to 0 --
061: * delay-unit CDATA #IMPLIED -- "seconds|minutes|hours|days", defaults to minutes --
062: * start-date CDATA #IMPLIED -- a date in form tt.mm.yyyy --
063: * start-time CDATA #IMPLIED -- a time in form hh:mm:ss --
064: * stop-date CDATA #IMPLIED -- a date in form tt.mm.yyyy --
065: * stop-time CDATA #IMPLIED -- a time in form hh:mm:ss --
066: * />
067: * </pre></code>
068: *
069: * @author <a href="mailto:gregor@webman.de">Gregor Klinke</a>
070: * @version $Revision: 1.2 $
071: **/
072: public class SchedulerMgr implements Manager {
073: /* $Id: SchedulerMgr.java,v 1.2 2002/04/12 12:45:53 gregor Exp $ */
074: /**
075: * logging facility
076: **/
077: private static Category cat = Category
078: .getInstance(SchedulerMgr.class);
079:
080: /**
081: * constants for time calculations (sec)
082: **/
083: public static final int MILLISECONDS_PER_SEC = 1000;
084: /**
085: * constants for time calculations (min)
086: **/
087: public static final int MILLISECONDS_PER_MIN = 1000 * 60;
088: /**
089: * constants for time calculations (hour)
090: **/
091: public static final int MILLISECONDS_PER_HOUR = 1000 * 60 * 60;
092: /**
093: * constants for time calculations (day)
094: **/
095: public static final int MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;
096:
097: /**
098: * date parser used for the date setting
099: **/
100: private static SimpleDateFormat date_format;
101:
102: /**
103: * date parser used for the time setting
104: **/
105: private static SimpleDateFormat time_format;
106:
107: /* ----------------------------------------------------------------------
108: creating and initializing
109: ---------------------------------------------------------------------- */
110: /* initialize the settings for date and time parsing */
111: static {
112: date_format = new SimpleDateFormat("dd.MM.yyyy");
113: date_format.setLenient(false);
114:
115: time_format = new SimpleDateFormat("HH:mm:ss");
116: time_format.setLenient(false);
117: }
118:
119: /**
120: * constructor reserved for used by the factory
121: * @param basedir the base directory to use for configuration
122: * @throws RegistryException if anything failed
123: **/
124: SchedulerMgr(String basedir) throws RegistryException {
125: cat.info("setup scheduler system");
126:
127: try {
128: File syncfile = new File(basedir,
129: "WEB-INF/classes/scheduler.xml");
130: readXMLStream(new BufferedInputStream(new FileInputStream(
131: syncfile)));
132:
133: // kick off!
134: startThread();
135:
136: cat.info("scheduler system started successfully");
137: } catch (SchedulerException se) {
138: throw new RegistryException(se);
139: } catch (IOException ioe) {
140: cat
141: .info("no 'scheduler.xml' found. No schedulers started");
142: }
143: }
144:
145: /* ----------------------------------------------------------------------
146: reading the configuration file and setting up the registered
147: schedulers
148: ---------------------------------------------------------------------- */
149: /* ----------------------------------------------------------------------
150: read configuration
151: ---------------------------------------------------------------------- */
152: /**
153: * reads a xml config file. for the xml structure see above
154: * @param in the stream to read the XML structure from
155: * @throws IOException guess what
156: * @throws SchedulerException something wrong with the scheduler setup
157: * @throws RegistryException something generaly wrong (mostly: can't find
158: * XML file, etc.)
159: **/
160: private void readXMLStream(BufferedInputStream in)
161: throws IOException, RegistryException, SchedulerException {
162: DocumentBuilderFactory factory = DocumentBuilderFactory
163: .newInstance();
164: factory.setNamespaceAware(false);
165: factory.setValidating(false);
166:
167: Document doc = null;
168: try {
169: DocumentBuilder builder = factory.newDocumentBuilder();
170: doc = builder.parse(new InputSource(in));
171: } catch (ParserConfigurationException pce) {
172: throw new RegistryException(pce);
173: } catch (SAXException se) {
174: throw new RegistryException(se);
175: }
176:
177: Node c0 = doc.getDocumentElement();
178: if (!("scheduler".equals(c0.getNodeName())))
179: throw new RegistryException("bad root element '"
180: + c0.getNodeName() + "'");
181:
182: for (Node c1 = c0.getFirstChild(); c1 != null; c1 = c1
183: .getNextSibling()) {
184: if (c1.getNodeType() == Node.ELEMENT_NODE) {
185: if ("service".equals(c1.getNodeName())) {
186: Element ce = (Element) c1;
187: HashMap prms = null;
188:
189: /* read parameters */
190: for (Node c2 = c1.getFirstChild(); c2 != null; c2 = c2
191: .getNextSibling()) {
192: if (c2.getNodeType() == Node.ELEMENT_NODE) {
193: if ("param".equals(c2.getNodeName())) {
194: Element c2e = (Element) c2;
195:
196: String key = c2e.getAttribute("key");
197: String value = c2e
198: .getAttribute("value");
199:
200: if (prms == null)
201: prms = new HashMap();
202:
203: prms.put(key, value);
204: } else
205: throw new RegistryException(
206: "unknown element: '"
207: + c2.getNodeName()
208: + "'");
209: }
210: }
211:
212: setupSchedulerService(ce, prms);
213: } else
214: throw new RegistryException("unknown element: '"
215: + c1.getNodeName() + "'");
216: }
217: }
218: }
219:
220: /**
221: * returns the first text element below a context node
222: * @param cntx the context node
223: **/
224: private String getTextData(Node cntx) {
225: cntx.normalize();
226:
227: for (Node n = cntx.getFirstChild(); n != null; n = n
228: .getNextSibling()) {
229: if (n.getNodeType() == Node.TEXT_NODE) {
230: return n.getNodeValue();
231: } else if (n.getNodeType() == Node.CDATA_SECTION_NODE) {
232: return n.getNodeValue();
233: }
234: }
235: return null;
236: }
237:
238: /**
239: * setup a specific scheduler service
240: * @param ce the dom node to extract the parameters from
241: * @param params the preparsed parameters found below the the domnode
242: * @return <code>true</code> if the setup was successfull
243: **/
244: private boolean setupSchedulerService(Element ce, Map params)
245: throws RegistryException, SchedulerException {
246: SchedulerServiceFactory sfact = null;
247: Date start_at = null;
248: Date stop_at = null;
249: long frequency = 0;
250: long delay = 0;
251: boolean start_once = false;
252: SchedulerService service_impl = null;
253:
254: String id = ce.getAttribute("id").trim();
255: String freq_s = ce.getAttribute("frequency").trim();
256: String freq_unit_s = ce.getAttribute("freq-unit").trim();
257: String delay_s = ce.getAttribute("delay").trim();
258: String delay_unit = ce.getAttribute("delay-unit").trim();
259: String start_date_s = ce.getAttribute("start-date").trim();
260: String start_time_s = ce.getAttribute("start-time").trim();
261: String stop_date_s = ce.getAttribute("stop-date").trim();
262: String stop_time_s = ce.getAttribute("stop-time").trim();
263: String fact_s = ce.getAttribute("factory-class").trim();
264:
265: /* [1] load the factory class */
266: try {
267: Class fact = Class.forName(fact_s.trim());
268: sfact = (SchedulerServiceFactory) fact.newInstance();
269: } catch (Exception e) {
270: cat.error("Can't load scheduler service: '" + fact_s
271: + "' (" + e + ")");
272: return false;
273: }
274:
275: /* [2] parse the start information */
276: start_at = parsePointOfTime(start_date_s, start_time_s,
277: new Date());
278: if (start_at == null)
279: return false;
280:
281: /* [2] parse the start information */
282: stop_at = parsePointOfTime(stop_date_s, stop_time_s, null);
283:
284: /* [3] parse the frequency information */
285: start_once = "ONCE".equals(freq_s.toUpperCase());
286: if (!start_once) {
287: try {
288: frequency = parseTimeWithUnit(freq_s, freq_unit_s);
289: } catch (NumberFormatException nfe) {
290: cat.error("Bad frequency setting: '" + freq_s + " "
291: + freq_unit_s + "' for scheduler service '"
292: + id + "' (" + nfe + ")");
293: return false;
294: }
295:
296: if (frequency <= 0) {
297: cat
298: .error("bad or no time setting for scheduler service '"
299: + id + "'");
300: return false;
301: }
302:
303: try {
304: delay = parseTimeWithUnit(delay_s, delay_unit);
305: } catch (NumberFormatException nfe) {
306: cat.error("Bad delay setting: '" + delay_s + " "
307: + delay_unit + "' for scheduler service '" + id
308: + "' (" + nfe + ")");
309: return false;
310: }
311: }
312:
313: /* [4] set the params to the factory */
314: if (params != null) {
315: for (Iterator it = params.keySet().iterator(); it.hasNext();) {
316: String key = (String) it.next();
317: sfact.setProperty(key, params.get(key));
318: }
319: }
320:
321: /* [5] register the service */
322: if (start_once)
323: registerOneTimeService(id, start_at, sfact);
324: else
325: registerFrequentService(id, start_at, stop_at, frequency,
326: delay, sfact);
327:
328: return true;
329: }
330:
331: /**
332: * parses a date (format: dd.mm.yyyy) and time (format: hh:mm:ss) string
333: * @param at_date the date string properly trimmed
334: * @param at_time the time string properly trimmed
335: * @return the resulting date or <code>null</code> if a parse exception
336: * (or similar) occured
337: * @param def the default date to use if no valid date could be parsed
338: * @return the date recognized
339: **/
340: private Date parsePointOfTime(String at_date, String at_time,
341: Date def) {
342: Date start_at_date = null;
343: Date start_at_time = null;
344:
345: /* [2] parse the date and time information */
346: if (at_date != null && at_date.length() > 0) {
347: try {
348: start_at_date = date_format.parse(at_date);
349: } catch (ParseException pe) {
350: cat.error("bad date setting: '" + pe + "'");
351: return null;
352: }
353: }
354:
355: if (at_time != null && at_time.length() > 0) {
356: try {
357: start_at_time = time_format.parse(at_time);
358: } catch (ParseException pe) {
359: cat.error("bad time setting: '" + pe + "'");
360: return null;
361: }
362: }
363:
364: if (start_at_date != null && start_at_time != null) {
365: Calendar dcal = Calendar.getInstance();
366: Calendar tcal = Calendar.getInstance();
367: dcal.setTime(start_at_date);
368: tcal.setTime(start_at_time);
369:
370: return new GregorianCalendar(dcal.get(Calendar.YEAR), dcal
371: .get(Calendar.MONTH), dcal
372: .get(Calendar.DAY_OF_MONTH), tcal
373: .get(Calendar.HOUR_OF_DAY), tcal
374: .get(Calendar.MINUTE), tcal.get(Calendar.SECOND))
375: .getTime();
376: } else if (start_at_date != null) {
377: return start_at_date;
378: } else if (start_at_time != null) {
379: Calendar dcal = Calendar.getInstance(); // relative to now!
380: Calendar tcal = Calendar.getInstance();
381: tcal.setTime(start_at_time);
382: return new GregorianCalendar(dcal.get(Calendar.YEAR), dcal
383: .get(Calendar.MONTH), dcal
384: .get(Calendar.DAY_OF_MONTH), tcal
385: .get(Calendar.HOUR_OF_DAY), tcal
386: .get(Calendar.MINUTE), tcal.get(Calendar.SECOND))
387: .getTime();
388: }
389:
390: return def;
391: }
392:
393: /**
394: * parses a gap-of-time information according to a unit. The units
395: * known are: minutes, seconds, days, hours
396: * @param tmstr the gap-of-time string
397: * @param unit the unit string, if <code>null</code> or empty defaults to "minute"
398: * @return the time computed to milliseconds
399: *
400: * @throws NumberFormatException if the timestring was bad or an
401: * unknown unit was used.
402: **/
403: private long parseTimeWithUnit(String tmstr, String unit)
404: throws NumberFormatException {
405: long tm = Long.parseLong(tmstr);
406:
407: if (unit != null && unit.length() > 0) {
408: if ("SECONDS".equals(unit.toUpperCase()))
409: tm *= MILLISECONDS_PER_SEC;
410: else if ("MINUTES".equals(unit.toUpperCase()))
411: tm *= MILLISECONDS_PER_MIN;
412: else if ("HOURS".equals(unit.toUpperCase()))
413: tm *= MILLISECONDS_PER_HOUR;
414: else if ("DAYS".equals(unit.toUpperCase()))
415: tm *= MILLISECONDS_PER_DAY;
416: else
417: throw new NumberFormatException("unknown time unit: '"
418: + unit + "'");
419: } else
420: tm *= MILLISECONDS_PER_MIN;
421:
422: return tm;
423: }
424:
425: /* ----------------------------------------------------------------------
426: the scheduler master thread stuff
427: ---------------------------------------------------------------------- */
428: /**
429: * default sleep time
430: **/
431: private final static long DEFAULT_CHECK_TIME = 10 * 1000; // 5 second
432:
433: /**
434: * minimum check time
435: **/
436: private final static long MIN_CHECK_TIME = 1000; // 1 second
437:
438: /**
439: * the list of schedulers
440: **/
441: private ArrayList schedulers = new ArrayList();
442:
443: /**
444: * the iterator for the scheduler checking
445: **/
446: private Iterator checker = null;
447:
448: /**
449: * the scheduler thread
450: **/
451: private SchedulerThread thread = null;
452:
453: /**
454: * the time between two checks for due schedulers
455: **/
456: private long sleepTime = DEFAULT_CHECK_TIME;
457:
458: /**
459: * kicks off the scheduling check thread
460: **/
461: private void startThread() {
462: if (thread == null && schedulers.size() > 0) {
463: thread = new SchedulerThread(this );
464: Thread t = new Thread(thread);
465:
466: /* the timer thread is a time daemon only. */
467: t.setDaemon(true);
468: t.start();
469: }
470: }
471:
472: /**
473: * checks the next scheduler in the scheduler list
474: **/
475: void checkNext() {
476: cat.info("check next");
477: synchronized (schedulers) {
478: if (checker == null || !checker.hasNext()) {
479: checker = schedulers.iterator();
480: if (!checker.hasNext()) {
481: thread.stop();
482: thread = null;
483: sleepTime = DEFAULT_CHECK_TIME;
484: }
485: }
486:
487: ServiceEntry srve = (ServiceEntry) checker.next();
488: Date now = new Date();
489: if (srve.isDue(now))
490: srve.executeNewService();
491:
492: if (srve.isOutdated(now)) {
493: cat
494: .debug("remove outdated service + '" + srve.id
495: + "'");
496: checker.remove();
497: }
498: }
499: }
500:
501: /**
502: * returns the current sleep time for the timer thread
503: **/
504: long getSleepTime() {
505: return sleepTime;
506: }
507:
508: /**
509: * adds a service entry to the list of services.
510: * @param se the service entry to add, must not be <code>null</code>
511: * @param _hint a time hint to use. this is normaly the frequency, in
512: * which the service should be used.
513: **/
514: private void addService(ServiceEntry se, long _hint) {
515: synchronized (schedulers) {
516: cat.info("register scheduler service '" + se.id + "' ("
517: + se.factory.getClass().getName() + ") hint at: "
518: + _hint);
519: schedulers.add(se);
520:
521: /* if the hinted time frame is smalled than the actually sleep
522: time frame, use this one */
523: if (_hint < sleepTime) {
524: if (_hint > 0)
525: sleepTime = _hint + MIN_CHECK_TIME;
526: else
527: sleepTime = MIN_CHECK_TIME;
528: }
529: if (thread == null)
530: startThread();
531:
532: /* adding an object, makes the iterator unusable */
533: checker = null;
534: }
535: }
536:
537: /**
538: * registers a scheduler service, to be called on a regular base. This
539: * frequent repetition is characteristized by a start and stop point in
540: * time, a start off delay and a repetition frequency.
541: * @param _id a descriptive id of the service
542: * @param _start_at start point
543: * @param _stop_at stop point, if <code>null</code> stops never
544: * @param _frequency the repetition frequency in milliseconds
545: * @param _delay the start off delay in milliseconds, relative to _start_at
546: * @param _sfact the scheduler services factory class
547: **/
548: public void registerFrequentService(String _id, Date _start_at,
549: Date _stop_at, long _frequency, long _delay,
550: SchedulerServiceFactory _sfact) {
551: addService(new FrequentService(_id, _start_at, _stop_at,
552: _frequency, _delay, _sfact), _frequency);
553: }
554:
555: /**
556: * registers a scheduler service, to be called excatly once, sometime
557: * in the future.
558: * @param _id a descriptive id of the service
559: * @param _start_at the point in time to start the service
560: * @param _sfact the scheduler services factory class
561: **/
562: public void registerOneTimeService(String _id, Date _start_at,
563: SchedulerServiceFactory _sfact) {
564: Calendar cal = Calendar.getInstance();
565: cal.setTime(_start_at);
566: long check_time = System.currentTimeMillis()
567: - cal.getTime().getTime();
568:
569: addService(new OneTimeService(_id, _start_at, _sfact),
570: check_time);
571: }
572: }
|