Categories
Java

Handling Unix’ cron-like information in Java

I recently had the requirement to write scheduled tasks within Java. I decided to use cron-like syntax in order to define when a task should run. A special class was created handling the scheduling information. Here it is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
package mypackage;
 
import java.util.Calendar;
import java.util.GregorianCalendar;
 
/**
 * Provides cron-like scheduling information.
 * This class implements cron-like definition of scheduling information.
 * Various methods can be used to check whether a timestamp matches the
 * schedule or not. However, there is a slight difference between cron and
 * this class. Cron describes a match when either the day of month and month
 * or the day of week are met. This class requires both to be met for a match.
 * Also note that Calendar defines Sunday through Saturday with 1 through 7 respectively
 * @author RalphSchuster
 *
 */
public class CronSchedule {
 
	/**
	 * Types being used.
	 * This array defines the types and their indices.
	 */
	protected static int TYPES[] = new int[] {
		Calendar.MINUTE, Calendar.HOUR_OF_DAY, 
		Calendar.DAY_OF_MONTH, Calendar.MONTH, 
		Calendar.DAY_OF_WEEK
	};
 
	private AbstractTimeValue timeValues[][] = new AbstractTimeValue[TYPES.length][];
 
	/**
	 * Default constructor
	 * Constructor with all terms set to "*".
	 */
	public CronSchedule() {
		this("*", "*", "*", "*", "*");
	}
 
	/**
	 * Constructor with cron-style string initialization.
	 * The cron style is: $minute $hour $dayOfMonth $month $dayOfWeek
	 * @param schedule
	 */
	public CronSchedule(String schedule) {
		set(schedule);
	}
 
	/**
	 * Constructor with separate initialization values.
	 * @param min - minute definition
	 * @param hour - hour definition
	 * @param dom - day of month definition
	 * @param mon - month definition
	 * @param dow - day of week definition
	 */
	public CronSchedule(String min, String hour, String dom, String mon, String dow) {
		set(Calendar.MINUTE, min);
		set(Calendar.HOUR_OF_DAY, hour);
		set(Calendar.DAY_OF_MONTH, dom);
		set(Calendar.MONTH, mon);
		set(Calendar.DAY_OF_WEEK, dow);
	}
 
	/**
	 * Sets the cron schedule.
	 * The cron style is: $minute $hour $dayOfMonth $month $dayOfWeek
	 * The function will return any characters that follow the cron definition
	 * @param schedule - cron-like schedule definition
	 * @return characters following the cron definition.
	 */
	public String set(String schedule) {
		String parts[] = schedule.split(" ", TYPES.length+1);
		if (parts.length < TYPES.length) throw new IllegalArgumentException("Invalid cron format: "+schedule);
		for (int i=0; i<TYPES.length; i++) set(getType(i), parts[i]);
		return parts.length > TYPES.length ? parts[TYPES.length] : null;
	}
 
	/**
	 * Sets the time values accordingly
	 * @param type - Calendar constant to define what values will be set
	 * @param values - comma-separated list of definitions for that type
	 */
	public void set(int type, String values) {
		// Split the values
		String parts[] = values.split(",");
		AbstractTimeValue result[] = new AbstractTimeValue[parts.length];
 
		// Iterate over entries
		for (int i=0; i<parts .length; i++) {
			// Decide what time value is set and create it
			if (parts[i].indexOf("/") > 0) result[i] = new TimeSteps(parts[i]);
			else if (parts[i].indexOf("-") > 0) result[i] = new TimeRange(parts[i]);
			else if (parts[i].equals("*")) result[i] = new TimeAll();
			else result[i] = new SingleTimeValue(parts[i]);
		}
 
		// Save the array
		set(type, result);
	}
 
	/**
	 * Sets the values for a specific type
	 * @param type - Calendar constant defining the time type
	 * @param values - values to be set
	 */
	protected void set(int type, AbstractTimeValue values[]) {
		timeValues[getIndex(type)] = values;
	}
 
	/**
	 * Returns the values for a specific time type
	 * @param type - Calendar constant defining the type
	 * @return time value definitions
	 */
	protected AbstractTimeValue[] getValues(int type) {
		return timeValues[getIndex(type)];
	}
 
	/**
	 * Returns the cron-like definition string for the given time value
	 * @param type - Calendar constant defining time type
	 * @return cron-like definition
	 */
	public String get(int type) {
		AbstractTimeValue values[] = getValues(type);
		String rc = "";
		for (int i=0; i<values .length; i++) rc += ","+values[i].toString();
		return rc.substring(1);
	}
 
	/**
	 * Returns the cron-like definition of the schedule.
	 */
	public String toString() {
		String rc = "";
		for (int i=0; i<TYPES.length; i++) {
			rc += " "+get(getType(i));
		}
		return rc.trim();
	}
 
	/**
	 * Checks whether given timestamp matches with defined schedule.
	 * This is default check method. All criteria must be met including seconds to be 0.
	 * @param timeStamp - time in ms since Epoch time
	 * @return true when schedule matches
	 */
	public boolean matches(long timeStamp) {
		return matches(getCalendar(timeStamp));
	}
 
	/**
	 * Checks whether given timestamp matches with defined schedule.
	 * This is default check method. All criteria must be met including seconds to be 0.
	 * @param cal - calendar date
	 * @return true when schedule matches
	 */
	public boolean matches(Calendar cal) {
		return isMinute(cal) && (cal.get(Calendar.SECOND) == 0);
	}
 
	/**
	 * Checks whether given timestamp matches with defined schedule.
	 * This method can be used when seconds are not relevant for matching.
	 * This is default check method.
	 * @param timeStamp - time in ms since Epoch time
	 * @return true when schedule matches
	 */
	public boolean isMinute(long timeStamp) {
		return isMinute(getCalendar(timeStamp));
	}
 
	/**
	 * Checks whether given calendar date matches with defined schedule.
	 * This method can be used when seconds are not relevant for matching.
	 * @param cal - calendar date
	 * @return true when schedule matches
	 */
	public boolean isMinute(Calendar cal) {
		return matches(Calendar.MINUTE, cal) && isHour(cal);
	}
 
	/**
	 * Checks whether given timestamp matches with defined hour schedule.
	 * This method can be used when minute definition is not relevant for matching.
	 * @param timestamp - time in ms since Epoch time
	 * @return true when schedule matches
	 */
	public boolean isHour(long timeStamp) {
		return isHour(getCalendar(timeStamp));
	}
 
	/**
	 * Checks whether given calendar date matches with defined hour schedule.
	 * This method can be used when minute definition is not relevant for matching.
	 * @param cal - calendar date
	 * @return true when schedule matches
	 */
	public boolean isHour(Calendar cal) {
		return matches(Calendar.HOUR_OF_DAY, cal) && isDay(cal);
	}
 
	/**
	 * Checks whether given timestamp matches with defined day schedule.
	 * This method can be used when minute and hour definitions are not relevant for matching.
	 * @param timestamp - time in ms since Epoch time
	 * @return true when schedule matches
	 */
	public boolean isDay(long timeStamp) {
		return isDay(getCalendar(timeStamp));
	}
 
	/**
	 * Checks whether given calendar date matches with defined day schedule.
	 * This method can be used when minute and hour definitions are not relevant for matching.
	 * @param cal - calendar date
	 * @return true when schedule matches
	 */
	public boolean isDay(Calendar cal) {
		return 
			matches(Calendar.DAY_OF_WEEK, cal) &&
			matches(Calendar.DAY_OF_MONTH, cal) &&
			matches(Calendar.MONTH, cal);
	}
 
	/**
	 * Checks whether specific schedule definition matches against the given calendar date.
	 * @param type - Calendar constant defining time type to check for
	 * @param calendar - calendar representing the date to check
	 * @return true when definition matches
	 */
	protected boolean matches(int type, Calendar calendar) {
		// get the definitions and the comparison value
		AbstractTimeValue defs[] = timeValues[getIndex(type)];
		int value = calendar.get(type);
 
		// Any of the criteria must be met
		for (int i=0; i<defs.length; i++) {
			if (defs[i].matches(value)) return true;
		}
		return false;
	}
 
	/**
	 * Creates the calendar for a timestamp.
	 * @param timeStamp - timestamp
	 * @return calendar
	 */
	protected Calendar getCalendar(long timeStamp) {
		Calendar rc = new GregorianCalendar();
		rc.setTimeInMillis(timeStamp);
		return rc;
	}
 
	/**
	 * Returns the type at the specified index
	 * @param index - index
	 * @return Calendar constant of type
	 */
	protected static int getType(int index) {
		return TYPES[index];
	}
 
	/**
	 * Returns the index for the specified Calendar type.
	 * @param type - Calendar constant for type
	 * @return internal index
	 */
	protected static int getIndex(int type) {
		for (int i=0; i<TYPES.length; i++) {
			if (TYPES[i] == type) return i;
		}
		throw new IllegalArgumentException("No such time type: "+type);
	}
 
	/**
	 * Base class for timing values.
	 * @author RalphSchuster
	 */
	public static abstract class AbstractTimeValue {
 
		/**
		 * Returns true when given time value matches defined time.
		 * @param timeValue - time value to evaluate
		 * @return true when time matches
		 */
		public abstract boolean matches(int timeValue);
	}
 
	/**
	 * Represents a single time value, e.g. 9
	 * @author RalphSchuster
	 */
	public static class SingleTimeValue extends AbstractTimeValue {
 
		private int value;
 
		public SingleTimeValue(int value) {
			setValue(value);
		}
 
		public SingleTimeValue(String value) {
			setValue(Integer.parseInt(value));
		}
 
		/**
		 * @return the value
		 */
		public int getValue() {
			return value;
		}
 
		/**
		 * @param value the value to set
		 */
		public void setValue(int value) {
			this.value = value;
		}
 
		/**
		 * Returns true when given time value matches defined value.
		 * @param timeValue - time value to evaluate
		 * @return true when time matches
		 */
		public boolean matches(int timeValue) {
			return timeValue == getValue();
		}
 
		/**
		 * Returns cron-like string of this definition.
		 */
		public String toString() {
			return ""+getValue();
		}
	}
 
	/**
	 * Represents a time range, e.g. 5-9
	 * @author RalphSchuster
	 */
	public static class TimeRange extends AbstractTimeValue {
 
		private int startValue;
		private int endValue;
 
		public TimeRange(int startValue, int endValue) {
			setStartValue(startValue);
			setEndValue(endValue);
		}
 
		public TimeRange(String range) {
			int dashPos = range.indexOf("-");
			setStartValue(Integer.parseInt(range.substring(0, dashPos)));
			setEndValue(Integer.parseInt(range.substring(dashPos+1)));
		}
 
		/**
		 * @return the endValue
		 */
		public int getEndValue() {
			return endValue;
		}
 
		/**
		 * @param endValue the endValue to set
		 */
		public void setEndValue(int endValue) {
			this.endValue = endValue;
		}
 
		/**
		 * @return the startValue
		 */
		public int getStartValue() {
			return startValue;
		}
 
		/**
		 * @param startValue the startValue to set
		 */
		public void setStartValue(int startValue) {
			this.startValue = startValue;
		}
 
		/**
		 * Returns true when given time value falls in range.
		 * @param timeValue - time value to evaluate
		 * @return true when time falls in range
		 */
		public boolean matches(int timeValue) {
			return (getStartValue() <= timeValue) && (timeValue <= getEndValue());
		}
 
		/**
		 * Returns cron-like string of this definition.
		 */
		public String toString() {
			return getStartValue()+"-"+getEndValue();
		}
	}
 
	/**
	 * Represents a time interval, e.g. 0-4/10
	 * @author RalphSchuster
	 */
	public static class TimeSteps extends AbstractTimeValue {
 
		private AbstractTimeValue range;
		private int steps;
 
		public TimeSteps(AbstractTimeValue range, int steps) {
			setRange(range);
			setSteps(steps);
		}
 
		public TimeSteps(String def) {
			int divPos = def.indexOf("/");
			String r = def.substring(0, divPos);
			if (r.equals("*")) setRange(new TimeAll());
			else if (r.indexOf("-") > 0) setRange(new TimeRange(r));
			else throw new IllegalArgumentException("Invalid range: "+def);
			setSteps(Integer.parseInt(def.substring(divPos+1)));
		}
 
		/**
		 * Returns true when given time value matches the interval.
		 * @param timeValue - time value to evaluate
		 * @return true when time matches the interval
		 */
		public boolean matches(int timeValue) {
			boolean rc = getRange().matches(timeValue);
			if (rc) rc = timeValue % getSteps() == 0;
			return rc;
		}
 
		/**
		 * @return the range
		 */
		public AbstractTimeValue getRange() {
			return range;
		}
 
		/**
		 * @param range the range to set
		 */
		public void setRange(AbstractTimeValue range) {
			this.range = range;
		}
 
		/**
		 * @return the steps
		 */
		public int getSteps() {
			return steps;
		}
 
		/**
		 * @param steps the steps to set
		 */
		public void setSteps(int steps) {
			this.steps = steps;
		}
 
		/**
		 * Returns cron-like string of this definition.
		 */
		public String toString() {
			return getRange()+"/"+getSteps();
		}
 
	}
 
	/**
	 * Represents the ALL time, *.
	 * @author RalphSchuster
	 */
	public static class TimeAll extends AbstractTimeValue {
 
		public TimeAll() {
 
		}
 
		/**
		 * Returns always true.
		 * @param timeValue - time value to evaluate
		 * @return true 
		 */
		public boolean matches(int timeValue) {
			return true;
		}
 
		/**
		 * Returns cron-like string of this definition.
		 */
		public String toString() {
			return "*";
		}
	}
}

Some explanations how you can use this class:

  • Create the schedule by just passing in the cron-like definition to the constructor method.
  • You can also create a schedule by using the default constructor and then setting the schedule with set(String). The advantage of this method is that it will return you any additional information that follows the schedule definition, e.g. as you can find it in /etc/crontab.
  • Beware that Java defines Sunday as 1, Saturday as 7. The real crontab ranges from 0-6 (7 as sunday, too)
  • matches(long) (line 149) is the default method to check whether a timestamp fulfills the requirements of the schedule definition. This method always checks whether the second of the timestamp is 0 (no millisecond check!).
  • isMinute(long), isHour(long), isDay(long) check the given timestamp only if the respective time requirement is met. That mean isMinute() will ignore seconds, isHour() will ignore minutes and seconds, isDay() will ignore time of the day.
  • Override getCalendar(long) (line 251) when you don’t wanna use the Gregorian Calendar or need a different timezone than your current default timezone.
  • You can easily add more time value checks (e.g. week of the year) by simply enhancing the TYPES array (line 23)