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 }