001    /* ============================================================
002     * JRobin : Pure java implementation of RRDTool's functionality
003     * ============================================================
004     *
005     * Project Info:  http://www.jrobin.org
006     * Project Lead:  Sasa Markovic (saxon@jrobin.org);
007     *
008     * (C) Copyright 2003-2005, by Sasa Markovic.
009     *
010     * Developers:    Sasa Markovic (saxon@jrobin.org)
011     *
012     *
013     * This library is free software; you can redistribute it and/or modify it under the terms
014     * of the GNU Lesser General Public License as published by the Free Software Foundation;
015     * either version 2.1 of the License, or (at your option) any later version.
016     *
017     * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
018     * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
019     * See the GNU Lesser General Public License for more details.
020     *
021     * You should have received a copy of the GNU Lesser General Public License along with this
022     * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
023     * Boston, MA 02111-1307, USA.
024     */
025    
026    /*
027     * Java port of Tobi's original parsetime.c routine
028     */
029    package org.jrobin.core.timespec;
030    
031    import org.jrobin.core.RrdException;
032    import org.jrobin.core.Util;
033    
034    /**
035     * Class which parses at-style time specification (describided in detail on the rrdfetch man page),
036     * used in all RRDTool commands. This code is in most parts just a java port of Tobi's parsetime.c
037     * code.
038     */
039    public class TimeParser {
040            private static final int PREVIOUS_OP = -1;
041    
042            TimeToken token;
043            TimeScanner scanner;
044            TimeSpec spec;
045    
046            int op = TimeToken.PLUS;
047            int prev_multiplier = -1;
048    
049            /**
050             * Constructs TimeParser instance from the given input string.
051             *
052             * @param dateString at-style time specification (read rrdfetch man page
053             *                   for the complete explanation)
054             */
055            public TimeParser(String dateString) {
056                    scanner = new TimeScanner(dateString);
057                    spec = new TimeSpec(dateString);
058            }
059    
060            private void expectToken(int desired, String errorMessage) throws RrdException {
061                    token = scanner.nextToken();
062                    if (token.id != desired) {
063                            throw new RrdException(errorMessage);
064                    }
065            }
066    
067            private void plusMinus(int doop) throws RrdException {
068                    if (doop >= 0) {
069                            op = doop;
070                            expectToken(TimeToken.NUMBER, "There should be number after " +
071                                            (op == TimeToken.PLUS ? '+' : '-'));
072                            prev_multiplier = -1; /* reset months-minutes guessing mechanics */
073                    }
074                    int delta = Integer.parseInt(token.value);
075                    token = scanner.nextToken();
076                    if (token.id == TimeToken.MONTHS_MINUTES) {
077                            /* hard job to guess what does that -5m means: -5mon or -5min? */
078                            switch (prev_multiplier) {
079                                    case TimeToken.DAYS:
080                                    case TimeToken.WEEKS:
081                                    case TimeToken.MONTHS:
082                                    case TimeToken.YEARS:
083                                            token = scanner.resolveMonthsMinutes(TimeToken.MONTHS);
084                                            break;
085                                    case TimeToken.SECONDS:
086                                    case TimeToken.MINUTES:
087                                    case TimeToken.HOURS:
088                                            token = scanner.resolveMonthsMinutes(TimeToken.MINUTES);
089                                            break;
090                                    default:
091                                            if (delta < 6) {
092                                                    token = scanner.resolveMonthsMinutes(TimeToken.MONTHS);
093                                            }
094                                            else {
095                                                    token = scanner.resolveMonthsMinutes(TimeToken.MINUTES);
096                                            }
097                            }
098                    }
099                    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    }