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 }