View Javadoc

1   /* ============================================================
2    * JRobin : Pure java implementation of RRDTool's functionality
3    * ============================================================
4    *
5    * Project Info:  http://www.jrobin.org
6    * Project Lead:  Sasa Markovic (saxon@jrobin.org);
7    *
8    * (C) Copyright 2003-2005, by Sasa Markovic.
9    *
10   * Developers:    Sasa Markovic (saxon@jrobin.org)
11   *
12   *
13   * This library is free software; you can redistribute it and/or modify it under the terms
14   * of the GNU Lesser General Public License as published by the Free Software Foundation;
15   * either version 2.1 of the License, or (at your option) any later version.
16   *
17   * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
18   * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
19   * See the GNU Lesser General Public License for more details.
20   *
21   * You should have received a copy of the GNU Lesser General Public License along with this
22   * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
23   * Boston, MA 02111-1307, USA.
24   */
25  
26  /*
27   * Java port of Tobi's original parsetime.c routine
28   */
29  package org.jrobin.core.timespec;
30  
31  import org.jrobin.core.RrdException;
32  import org.jrobin.core.Util;
33  
34  /**
35   * Class which parses at-style time specification (describided in detail on the rrdfetch man page),
36   * used in all RRDTool commands. This code is in most parts just a java port of Tobi's parsetime.c
37   * code.
38   */
39  public class TimeParser {
40  	private static final int PREVIOUS_OP = -1;
41  
42  	TimeToken token;
43  	TimeScanner scanner;
44  	TimeSpec spec;
45  
46  	int op = TimeToken.PLUS;
47  	int prev_multiplier = -1;
48  
49  	/**
50  	 * Constructs TimeParser instance from the given input string.
51  	 *
52  	 * @param dateString at-style time specification (read rrdfetch man page
53  	 *                   for the complete explanation)
54  	 */
55  	public TimeParser(String dateString) {
56  		scanner = new TimeScanner(dateString);
57  		spec = new TimeSpec(dateString);
58  	}
59  
60  	private void expectToken(int desired, String errorMessage) throws RrdException {
61  		token = scanner.nextToken();
62  		if (token.id != desired) {
63  			throw new RrdException(errorMessage);
64  		}
65  	}
66  
67  	private void plusMinus(int doop) throws RrdException {
68  		if (doop >= 0) {
69  			op = doop;
70  			expectToken(TimeToken.NUMBER, "There should be number after " +
71  					(op == TimeToken.PLUS ? '+' : '-'));
72  			prev_multiplier = -1; /* reset months-minutes guessing mechanics */
73  		}
74  		int delta = Integer.parseInt(token.value);
75  		token = scanner.nextToken();
76  		if (token.id == TimeToken.MONTHS_MINUTES) {
77  			/* hard job to guess what does that -5m means: -5mon or -5min? */
78  			switch (prev_multiplier) {
79  				case TimeToken.DAYS:
80  				case TimeToken.WEEKS:
81  				case TimeToken.MONTHS:
82  				case TimeToken.YEARS:
83  					token = scanner.resolveMonthsMinutes(TimeToken.MONTHS);
84  					break;
85  				case TimeToken.SECONDS:
86  				case TimeToken.MINUTES:
87  				case TimeToken.HOURS:
88  					token = scanner.resolveMonthsMinutes(TimeToken.MINUTES);
89  					break;
90  				default:
91  					if (delta < 6) {
92  						token = scanner.resolveMonthsMinutes(TimeToken.MONTHS);
93  					}
94  					else {
95  						token = scanner.resolveMonthsMinutes(TimeToken.MINUTES);
96  					}
97  			}
98  		}
99  		prev_multiplier = token.id;
100 		delta *= (op == TimeToken.PLUS) ? +1 : -1;
101 		switch (token.id) {
102 			case TimeToken.YEARS:
103 				spec.dyear += delta;
104 				break;
105 			case TimeToken.MONTHS:
106 				spec.dmonth += delta;
107 				break;
108 			case TimeToken.WEEKS:
109 				delta *= 7;
110 				/* FALLTHRU */
111 			case TimeToken.DAYS:
112 				spec.dday += delta;
113 				break;
114 			case TimeToken.HOURS:
115 				spec.dhour += delta;
116 				break;
117 			case TimeToken.MINUTES:
118 				spec.dmin += delta;
119 				break;
120 			case TimeToken.SECONDS:
121 			default: // default is 'seconds'
122 				spec.dsec += delta;
123 				break;
124 		}
125 		// unreachable statement
126 		// throw new RrdException("Well-known time unit expected after " + delta);
127 	}
128 
129 	private void timeOfDay() throws RrdException {
130 		int hour, minute = 0;
131 		/* save token status in case we must abort */
132 		scanner.saveState();
133 		/* first pick out the time of day - we assume a HH (COLON|DOT) MM time */
134 		if (token.value.length() > 2) {
135 			return;
136 		}
137 		hour = Integer.parseInt(token.value);
138 		token = scanner.nextToken();
139 		if (token.id == TimeToken.SLASH || token.id == TimeToken.DOT) {
140 			/* guess we are looking at a date */
141 			token = scanner.restoreState();
142 			return;
143 		}
144 		if (token.id == TimeToken.COLON) {
145 			expectToken(TimeToken.NUMBER, "Parsing HH:MM syntax, expecting MM as number, got none");
146 			minute = Integer.parseInt(token.value);
147 			if (minute > 59) {
148 				throw new RrdException("Parsing HH:MM syntax, got MM = " +
149 						minute + " (>59!)");
150 			}
151 			token = scanner.nextToken();
152 		}
153 		/* check if an AM or PM specifier was given */
154 		if (token.id == TimeToken.AM || token.id == TimeToken.PM) {
155 			if (hour > 12) {
156 				throw new RrdException("There cannot be more than 12 AM or PM hours");
157 			}
158 			if (token.id == TimeToken.PM) {
159 				if (hour != 12) {
160 					/* 12:xx PM is 12:xx, not 24:xx */
161 					hour += 12;
162 				}
163 			}
164 			else {
165 				if (hour == 12) {
166 					/* 12:xx AM is 00:xx, not 12:xx */
167 					hour = 0;
168 				}
169 			}
170 			token = scanner.nextToken();
171 		}
172 		else if (hour > 23) {
173 			/* guess it was not a time then ... */
174 			token = scanner.restoreState();
175 			return;
176 		}
177 		spec.hour = hour;
178 		spec.min = minute;
179 		spec.sec = 0;
180 		if (spec.hour == 24) {
181 			spec.hour = 0;
182 			spec.day++;
183 		}
184 	}
185 
186 	private void assignDate(long mday, long mon, long year) throws RrdException {
187 		if (year > 138) {
188 			if (year > 1970) {
189 				year -= 1900;
190 			}
191 			else {
192 				throw new RrdException("Invalid year " + year +
193 						" (should be either 00-99 or >1900)");
194 			}
195 		}
196 		else if (year >= 0 && year < 38) {
197 			year += 100;		 /* Allow year 2000-2037 to be specified as   */
198 		}						 /* 00-37 until the problem of 2038 year will */
199 		/* arise for unices with 32-bit time_t     */
200 		if (year < 70) {
201 			throw new RrdException("Won't handle dates before epoch (01/01/1970), sorry");
202 		}
203 		spec.year = (int) year;
204 		spec.month = (int) mon;
205 		spec.day = (int) mday;
206 	}
207 
208 	private void day() throws RrdException {
209 		long mday = 0, wday, mon, year = spec.year;
210 		switch (token.id) {
211 			case TimeToken.YESTERDAY:
212 				spec.day--;
213 				/* FALLTRHU */
214 			case TimeToken.TODAY:	/* force ourselves to stay in today - no further processing */
215 				token = scanner.nextToken();
216 				break;
217 			case TimeToken.TOMORROW:
218 				spec.day++;
219 				token = scanner.nextToken();
220 				break;
221 			case TimeToken.JAN:
222 			case TimeToken.FEB:
223 			case TimeToken.MAR:
224 			case TimeToken.APR:
225 			case TimeToken.MAY:
226 			case TimeToken.JUN:
227 			case TimeToken.JUL:
228 			case TimeToken.AUG:
229 			case TimeToken.SEP:
230 			case TimeToken.OCT:
231 			case TimeToken.NOV:
232 			case TimeToken.DEC:
233 				/* do month mday [year] */
234 				mon = (token.id - TimeToken.JAN);
235 				expectToken(TimeToken.NUMBER, "the day of the month should follow month name");
236 				mday = Long.parseLong(token.value);
237 				token = scanner.nextToken();
238 				if (token.id == TimeToken.NUMBER) {
239 					year = Long.parseLong(token.value);
240 					token = scanner.nextToken();
241 				}
242 				else {
243 					year = spec.year;
244 				}
245 				assignDate(mday, mon, year);
246 				break;
247 			case TimeToken.SUN:
248 			case TimeToken.MON:
249 			case TimeToken.TUE:
250 			case TimeToken.WED:
251 			case TimeToken.THU:
252 			case TimeToken.FRI:
253 			case TimeToken.SAT:
254 				/* do a particular day of the week */
255 				wday = (token.id - TimeToken.SUN);
256 				spec.day += (wday - spec.wday);
257 				token = scanner.nextToken();
258 				break;
259 			case TimeToken.NUMBER:
260 				/* get numeric <sec since 1970>, MM/DD/[YY]YY, or DD.MM.[YY]YY */
261 				// int tlen = token.value.length();
262 				mon = Long.parseLong(token.value);
263 				if (mon > 10L * 365L * 24L * 60L * 60L) {
264 					spec.localtime(mon);
265 					token = scanner.nextToken();
266 					break;
267 				}
268 				if (mon > 19700101 && mon < 24000101) { /*works between 1900 and 2400 */
269 					year = mon / 10000;
270 					mday = mon % 100;
271 					mon = (mon / 100) % 100;
272 					token = scanner.nextToken();
273 				}
274 				else {
275 					token = scanner.nextToken();
276 					if (mon <= 31 && (token.id == TimeToken.SLASH || token.id == TimeToken.DOT)) {
277 						int sep = token.id;
278 						expectToken(TimeToken.NUMBER, "there should be " +
279 								(sep == TimeToken.DOT ? "month" : "day") +
280 								" number after " +
281 								(sep == TimeToken.DOT ? '.' : '/'));
282 						mday = Long.parseLong(token.value);
283 						token = scanner.nextToken();
284 						if (token.id == sep) {
285 							expectToken(TimeToken.NUMBER, "there should be year number after " +
286 									(sep == TimeToken.DOT ? '.' : '/'));
287 							year = Long.parseLong(token.value);
288 							token = scanner.nextToken();
289 						}
290 						/* flip months and days for European timing */
291 						if (sep == TimeToken.DOT) {
292 							long x = mday;
293 							mday = mon;
294 							mon = x;
295 						}
296 					}
297 				}
298 				mon--;
299 				if (mon < 0 || mon > 11) {
300 					throw new RrdException("Did you really mean month " + (mon + 1));
301 				}
302 				if (mday < 1 || mday > 31) {
303 					throw new RrdException("I'm afraid that " + mday +
304 							" is not a valid day of the month");
305 				}
306 				assignDate(mday, mon, year);
307 				break;
308 		}
309 	}
310 
311 	/**
312 	 * Parses the input string specified in the constructor.
313 	 *
314 	 * @return Object representing parsed date/time.
315 	 * @throws RrdException Thrown if the date string cannot be parsed.
316 	 */
317 	public TimeSpec parse() throws RrdException {
318 		long now = Util.getTime();
319 		int hr = 0;
320 		/* this MUST be initialized to zero for midnight/noon/teatime */
321 		/* establish the default time reference */
322 		spec.localtime(now);
323 		token = scanner.nextToken();
324 		switch (token.id) {
325 			case TimeToken.PLUS:
326 			case TimeToken.MINUS:
327 				break; /* jump to OFFSET-SPEC part */
328 			case TimeToken.START:
329 				spec.type = TimeSpec.TYPE_START;
330 				/* FALLTHRU */
331 			case TimeToken.END:
332 				if (spec.type != TimeSpec.TYPE_START) {
333 					spec.type = TimeSpec.TYPE_END;
334 				}
335 				spec.year = spec.month = spec.day = spec.hour = spec.min = spec.sec = 0;
336 				/* FALLTHRU */
337 			case TimeToken.NOW:
338 				int time_reference = token.id;
339 				token = scanner.nextToken();
340 				if (token.id == TimeToken.PLUS || token.id == TimeToken.MINUS) {
341 					break;
342 				}
343 				if (time_reference != TimeToken.NOW) {
344 					throw new RrdException("Words 'start' or 'end' MUST be followed by +|- offset");
345 				}
346 				else if (token.id != TimeToken.EOF) {
347 					throw new RrdException("If 'now' is followed by a token it must be +|- offset");
348 				}
349 				break;
350 				/* Only absolute time specifications below */
351 			case TimeToken.NUMBER:
352 				timeOfDay();
353 				if (token.id != TimeToken.NUMBER) {
354 					break;
355 				}
356 				/* fix month parsing */
357 			case TimeToken.JAN:
358 			case TimeToken.FEB:
359 			case TimeToken.MAR:
360 			case TimeToken.APR:
361 			case TimeToken.MAY:
362 			case TimeToken.JUN:
363 			case TimeToken.JUL:
364 			case TimeToken.AUG:
365 			case TimeToken.SEP:
366 			case TimeToken.OCT:
367 			case TimeToken.NOV:
368 			case TimeToken.DEC:
369 				day();
370 				if (token.id != TimeToken.NUMBER) {
371 					break;
372 				}
373 				timeOfDay();
374 				break;
375 
376 				/* evil coding for TEATIME|NOON|MIDNIGHT - we've initialized
377 				 * hr to zero up above, then fall into this case in such a
378 				 * way so we add +12 +4 hours to it for teatime, +12 hours
379 				 * to it for noon, and nothing at all for midnight, then
380 				 * set our rettime to that hour before leaping into the
381 				 * month scanner
382 				 */
383 			case TimeToken.TEATIME:
384 				hr += 4;
385 				/* FALLTHRU */
386 			case TimeToken.NOON:
387 				hr += 12;
388 				/* FALLTHRU */
389 			case TimeToken.MIDNIGHT:
390 				spec.hour = hr;
391 				spec.min = 0;
392 				spec.sec = 0;
393 				token = scanner.nextToken();
394 				day();
395 				break;
396 			default:
397 				throw new RrdException("Unparsable time: " + token.value);
398 		}
399 
400 		/*
401 		 * the OFFSET-SPEC part
402 		 *
403 		 * (NOTE, the sc_tokid was prefetched for us by the previous code)
404 		 */
405 		if (token.id == TimeToken.PLUS || token.id == TimeToken.MINUS) {
406 			scanner.setContext(false);
407 			while (token.id == TimeToken.PLUS || token.id == TimeToken.MINUS ||
408 					token.id == TimeToken.NUMBER) {
409 				if (token.id == TimeToken.NUMBER) {
410 					plusMinus(PREVIOUS_OP);
411 				}
412 				else {
413 					plusMinus(token.id);
414 				}
415 				token = scanner.nextToken();
416 				/* We will get EOF eventually but that's OK, since
417 				token() will return us as many EOFs as needed */
418 			}
419 		}
420 		/* now we should be at EOF */
421 		if (token.id != TimeToken.EOF) {
422 			throw new RrdException("Unparsable trailing text: " + token.value);
423 		}
424 		return spec;
425 	}
426 }