/*******************************************************************************
* Copyright (c) 2001-2005 Sasa Markovic and Ciaran Treanor.
* Copyright (c) 2011 The OpenNMS Group, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*******************************************************************************/
/*
* Java port of Tobi's original parsetime.c routine
*/
package org.jrobin.core.timespec;
import org.jrobin.core.RrdException;
import org.jrobin.core.Util;
/**
* Class which parses at-style time specification (describided in detail on the rrdfetch man page),
* used in all RRDTool commands. This code is in most parts just a java port of Tobi's parsetime.c
* code.
*/
public class TimeParser {
private static final int PREVIOUS_OP = -1;
TimeToken token;
TimeScanner scanner;
TimeSpec spec;
int op = TimeToken.PLUS;
int prev_multiplier = -1;
/**
* Constructs TimeParser instance from the given input string.
*
* @param dateString at-style time specification (read rrdfetch man page
* for the complete explanation)
*/
public TimeParser(String dateString) {
scanner = new TimeScanner(dateString);
spec = new TimeSpec(dateString);
}
private void expectToken(int desired, String errorMessage) throws RrdException {
token = scanner.nextToken();
if (token.id != desired) {
throw new RrdException(errorMessage);
}
}
private void plusMinus(int doop) throws RrdException {
if (doop >= 0) {
op = doop;
expectToken(TimeToken.NUMBER, "There should be number after " +
(op == TimeToken.PLUS ? '+' : '-'));
prev_multiplier = -1; /* reset months-minutes guessing mechanics */
}
int delta = Integer.parseInt(token.value);
token = scanner.nextToken();
if (token.id == TimeToken.MONTHS_MINUTES) {
/* hard job to guess what does that -5m means: -5mon or -5min? */
switch (prev_multiplier) {
case TimeToken.DAYS:
case TimeToken.WEEKS:
case TimeToken.MONTHS:
case TimeToken.YEARS:
token = scanner.resolveMonthsMinutes(TimeToken.MONTHS);
break;
case TimeToken.SECONDS:
case TimeToken.MINUTES:
case TimeToken.HOURS:
token = scanner.resolveMonthsMinutes(TimeToken.MINUTES);
break;
default:
if (delta < 6) {
token = scanner.resolveMonthsMinutes(TimeToken.MONTHS);
}
else {
token = scanner.resolveMonthsMinutes(TimeToken.MINUTES);
}
}
}
prev_multiplier = token.id;
delta *= (op == TimeToken.PLUS) ? +1 : -1;
switch (token.id) {
case TimeToken.YEARS:
spec.dyear += delta;
break;
case TimeToken.MONTHS:
spec.dmonth += delta;
break;
case TimeToken.WEEKS:
delta *= 7;
spec.dday += delta;
break;
case TimeToken.DAYS:
spec.dday += delta;
break;
case TimeToken.HOURS:
spec.dhour += delta;
break;
case TimeToken.MINUTES:
spec.dmin += delta;
break;
case TimeToken.SECONDS:
default: // default is 'seconds'
spec.dsec += delta;
break;
}
// unreachable statement
// throw new RrdException("Well-known time unit expected after " + delta);
}
private void timeOfDay() throws RrdException {
int hour, minute = 0;
/* save token status in case we must abort */
scanner.saveState();
/* first pick out the time of day - we assume a HH (COLON|DOT) MM time */
if (token.value.length() > 2) {
return;
}
hour = Integer.parseInt(token.value);
token = scanner.nextToken();
if (token.id == TimeToken.SLASH || token.id == TimeToken.DOT) {
/* guess we are looking at a date */
token = scanner.restoreState();
return;
}
if (token.id == TimeToken.COLON) {
expectToken(TimeToken.NUMBER, "Parsing HH:MM syntax, expecting MM as number, got none");
minute = Integer.parseInt(token.value);
if (minute > 59) {
throw new RrdException("Parsing HH:MM syntax, got MM = " +
minute + " (>59!)");
}
token = scanner.nextToken();
}
/* check if an AM or PM specifier was given */
if (token.id == TimeToken.AM || token.id == TimeToken.PM) {
if (hour > 12) {
throw new RrdException("There cannot be more than 12 AM or PM hours");
}
if (token.id == TimeToken.PM) {
if (hour != 12) {
/* 12:xx PM is 12:xx, not 24:xx */
hour += 12;
}
}
else {
if (hour == 12) {
/* 12:xx AM is 00:xx, not 12:xx */
hour = 0;
}
}
token = scanner.nextToken();
}
else if (hour > 23) {
/* guess it was not a time then ... */
token = scanner.restoreState();
return;
}
spec.hour = hour;
spec.min = minute;
spec.sec = 0;
if (spec.hour == 24) {
spec.hour = 0;
spec.day++;
}
}
private void assignDate(long mday, long mon, long year) throws RrdException {
if (year > 138) {
if (year > 1970) {
year -= 1900;
}
else {
throw new RrdException("Invalid year " + year +
" (should be either 00-99 or >1900)");
}
}
else if (year >= 0 && year < 38) {
year += 100; /* Allow year 2000-2037 to be specified as */
} /* 00-37 until the problem of 2038 year will */
/* arise for unices with 32-bit time_t */
if (year < 70) {
throw new RrdException("Won't handle dates before epoch (01/01/1970), sorry");
}
spec.year = (int) year;
spec.month = (int) mon;
spec.day = (int) mday;
}
private void day() throws RrdException {
long mday = 0, wday, mon, year = spec.year;
switch (token.id) {
case TimeToken.YESTERDAY:
spec.day--;
token = scanner.nextToken();
break;
case TimeToken.TODAY: /* force ourselves to stay in today - no further processing */
token = scanner.nextToken();
break;
case TimeToken.TOMORROW:
spec.day++;
token = scanner.nextToken();
break;
case TimeToken.JAN:
case TimeToken.FEB:
case TimeToken.MAR:
case TimeToken.APR:
case TimeToken.MAY:
case TimeToken.JUN:
case TimeToken.JUL:
case TimeToken.AUG:
case TimeToken.SEP:
case TimeToken.OCT:
case TimeToken.NOV:
case TimeToken.DEC:
/* do month mday [year] */
mon = (token.id - TimeToken.JAN);
expectToken(TimeToken.NUMBER, "the day of the month should follow month name");
mday = Long.parseLong(token.value);
token = scanner.nextToken();
if (token.id == TimeToken.NUMBER) {
year = Long.parseLong(token.value);
token = scanner.nextToken();
}
else {
year = spec.year;
}
assignDate(mday, mon, year);
break;
case TimeToken.SUN:
case TimeToken.MON:
case TimeToken.TUE:
case TimeToken.WED:
case TimeToken.THU:
case TimeToken.FRI:
case TimeToken.SAT:
/* do a particular day of the week */
wday = (token.id - TimeToken.SUN);
spec.day += (wday - spec.wday);
token = scanner.nextToken();
break;
case TimeToken.NUMBER:
/* get numeric <sec since 1970>, MM/DD/[YY]YY, or DD.MM.[YY]YY */
// int tlen = token.value.length();
mon = Long.parseLong(token.value);
if (mon > 10L * 365L * 24L * 60L * 60L) {
spec.localtime(mon);
token = scanner.nextToken();
break;
}
if (mon > 19700101 && mon < 24000101) { /*works between 1900 and 2400 */
year = mon / 10000;
mday = mon % 100;
mon = (mon / 100) % 100;
token = scanner.nextToken();
}
else {
token = scanner.nextToken();
if (mon <= 31 && (token.id == TimeToken.SLASH || token.id == TimeToken.DOT)) {
int sep = token.id;
expectToken(TimeToken.NUMBER, "there should be " +
(sep == TimeToken.DOT ? "month" : "day") +
" number after " +
(sep == TimeToken.DOT ? '.' : '/'));
mday = Long.parseLong(token.value);
token = scanner.nextToken();
if (token.id == sep) {
expectToken(TimeToken.NUMBER, "there should be year number after " +
(sep == TimeToken.DOT ? '.' : '/'));
year = Long.parseLong(token.value);
token = scanner.nextToken();
}
/* flip months and days for European timing */
if (sep == TimeToken.DOT) {
long x = mday;
mday = mon;
mon = x;
}
}
}
mon--;
if (mon < 0 || mon > 11) {
throw new RrdException("Did you really mean month " + (mon + 1));
}
if (mday < 1 || mday > 31) {
throw new RrdException("I'm afraid that " + mday +
" is not a valid day of the month");
}
assignDate(mday, mon, year);
break;
}
}
/**
* Parses the input string specified in the constructor.
*
* @return Object representing parsed date/time.
* @throws RrdException Thrown if the date string cannot be parsed.
*/
public TimeSpec parse() throws RrdException {
long now = Util.getTime();
int hr = 0;
/* this MUST be initialized to zero for midnight/noon/teatime */
/* establish the default time reference */
spec.localtime(now);
token = scanner.nextToken();
switch (token.id) {
case TimeToken.PLUS:
case TimeToken.MINUS:
break; /* jump to OFFSET-SPEC part */
case TimeToken.START:
spec.type = TimeSpec.TYPE_START;
/* FALLTHRU */
case TimeToken.END:
if (spec.type != TimeSpec.TYPE_START) {
spec.type = TimeSpec.TYPE_END;
}
spec.year = spec.month = spec.day = spec.hour = spec.min = spec.sec = 0;
/* FALLTHRU */
case TimeToken.NOW:
int time_reference = token.id;
token = scanner.nextToken();
if (token.id == TimeToken.PLUS || token.id == TimeToken.MINUS) {
break;
}
if (time_reference != TimeToken.NOW) {
throw new RrdException("Words 'start' or 'end' MUST be followed by +|- offset");
}
else if (token.id != TimeToken.EOF) {
throw new RrdException("If 'now' is followed by a token it must be +|- offset");
}
break;
/* Only absolute time specifications below */
case TimeToken.NUMBER:
timeOfDay();
if (token.id != TimeToken.NUMBER) {
break;
}
/* fix month parsing */
case TimeToken.JAN:
case TimeToken.FEB:
case TimeToken.MAR:
case TimeToken.APR:
case TimeToken.MAY:
case TimeToken.JUN:
case TimeToken.JUL:
case TimeToken.AUG:
case TimeToken.SEP:
case TimeToken.OCT:
case TimeToken.NOV:
case TimeToken.DEC:
day();
if (token.id != TimeToken.NUMBER) {
break;
}
timeOfDay();
break;
/* evil coding for TEATIME|NOON|MIDNIGHT - we've initialized
* hr to zero up above, then fall into this case in such a
* way so we add +12 +4 hours to it for teatime, +12 hours
* to it for noon, and nothing at all for midnight, then
* set our rettime to that hour before leaping into the
* month scanner
*/
case TimeToken.TEATIME:
hr += 4;
/* FALLTHRU */
case TimeToken.NOON:
hr += 12;
/* FALLTHRU */
case TimeToken.MIDNIGHT:
spec.hour = hr;
spec.min = 0;
spec.sec = 0;
token = scanner.nextToken();
day();
break;
default:
throw new RrdException("Unparsable time: " + token.value);
}
/*
* the OFFSET-SPEC part
*
* (NOTE, the sc_tokid was prefetched for us by the previous code)
*/
if (token.id == TimeToken.PLUS || token.id == TimeToken.MINUS) {
scanner.setContext(false);
while (token.id == TimeToken.PLUS || token.id == TimeToken.MINUS ||
token.id == TimeToken.NUMBER) {
if (token.id == TimeToken.NUMBER) {
plusMinus(PREVIOUS_OP);
}
else {
plusMinus(token.id);
}
token = scanner.nextToken();
/* We will get EOF eventually but that's OK, since
token() will return us as many EOFs as needed */
}
}
/* now we should be at EOF */
if (token.id != TimeToken.EOF) {
throw new RrdException("Unparsable trailing text: " + token.value);
}
return spec;
}
}