/**
* Copyright 2010 The University of Nottingham
*
* This file is part of lobbyservice.
*
* lobbyservice is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* lobbyservice 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with lobbyservice. If not, see <http://www.gnu.org/licenses/>.
*
*/
package uk.ac.horizon.ug.lobby.server;
import java.io.StringWriter;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;
import java.util.SortedSet;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.logging.Logger;
import org.json.JSONArray;
import org.json.JSONString;
import org.json.JSONStringer;
import com.google.appengine.repackaged.org.json.JSONException;
import com.google.appengine.repackaged.org.json.JSONWriter;
import uk.ac.horizon.ug.lobby.protocol.GameTimeOption;
import uk.ac.horizon.ug.lobby.protocol.GameTimeOptions;
/**
* @author cmg
*
*/
public class FactoryUtils {
static Logger logger = Logger.getLogger(FactoryUtils.class.getName());
enum Parts {
SECONDS(0,59,Calendar.SECOND),
MINUTES(0,59,Calendar.MINUTE),
HOURS(0,23,Calendar.HOUR_OF_DAY),
DAY_OF_MONTH(1,31,Calendar.DAY_OF_MONTH),
MONTH(1,12,Calendar.MONTH),
DAY_OF_WEEK(1,7,Calendar.DAY_OF_WEEK),
YEAR(2010,2011,Calendar.YEAR); // cron range: 1970-2099
private int min; private int max; private int calendarIx;
Parts(int min, int max, int calendarIx) { this.min = min; this.max = max; this.calendarIx = calendarIx; }
public int min() { return min; }
public int max() { return max; }
public int calendarIx() { return calendarIx; }
public int getValue(Calendar cal) {
int val = cal.get(calendarIx);
if (this!=YEAR)
val = val-cal.getMinimum(calendarIx)+min;
return val;
}
public void setValue(Calendar cal, int val) {
if (this!=YEAR)
val = val-min+cal.getMinimum(calendarIx);
cal.set(calendarIx, val);
}
};
enum Months { MONTH_ZERO, JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC };
// fortunately Calendar also starts from SUNDAY
enum DaysOfWeek { DAY_ZERO, SUN, MON, TUE, WED, THU, FRI, SAT };
/** parse and return basic value set for part of cron expression */
private static TreeSet<Integer> parseValues(String text, Parts part) throws CronExpressionException {
TreeSet<Integer> values = new TreeSet<Integer>();
String atCommas[] = text.split("[,]");
for (int i=0; i<atCommas.length; i++)
{
String t2 = atCommas[i];
int slashIx = t2.lastIndexOf("/");
if (slashIx>=0) {
int interval = 0;
try {
interval = Integer.parseInt(t2.substring(slashIx+1));
}
catch (NumberFormatException nfe) {
throw new CronExpressionException("'/n' clause misformed in "+part+" of "+text);
}
if (interval<=0)
throw new CronExpressionException("'/n' clause with n <= 0 in "+part+" of "+text);
Set<Integer> baseValues = parseValues(text.substring(0, slashIx), part);
for (int offset=0; offset<=part.max(); offset+=interval) {
for (Integer baseValue : baseValues) {
if (baseValue + offset <= part.max())
values.add(baseValue+offset);
}
}
}
else {
int hyphIx = t2.indexOf("-");
if (hyphIx>=0) {
int from = parseSimpleValue(t2.substring(0, hyphIx), part);
int to = parseSimpleValue(t2.substring(0, hyphIx), part);
if (to<from)
throw new CronExpressionException("'n-m' clause with n>m in "+part+" of "+text);
for (int val = from; val<=to; val++) {
values.add(val);
}
}
else {
if (t2.equals("*") || t2.equals("?")) {
// all values
for (int val=part.min(); val<=part.max(); val++)
values.add(val);
}
else {
// simple value
values.add(parseSimpleValue(t2, part));
}
}
}
}
return values;
}
/**
* @param substring
* @param part
* @return
* @throws CronExpressionException
*/
private static int parseSimpleValue(String substring, Parts part) throws CronExpressionException {
if (part==Parts.DAY_OF_WEEK) {
for (int i=0; i<DaysOfWeek.values().length; i++)
if (substring.equals(DaysOfWeek.values()[i]))
return DaysOfWeek.values()[i].ordinal();
}
if (part==Parts.MONTH) {
for (int i=0; i<Months.values().length; i++)
if (substring.equals(Months.values()[i]))
return Months.values()[i].ordinal();
}
try {
int value = Integer.parseInt(substring);
if (value<part.min() || value>part.max())
throw new CronExpressionException(part+" value out of range: "+value+" ("+part.min()+"-"+part.max()+")");
return value;
}
catch (NumberFormatException nfe) {
throw new CronExpressionException(part+" value invalid: "+substring+" ("+part.min()+"-"+part.max()+")");
}
}
private static String [] getParts(String cronExpression) throws CronExpressionException {
String parts[] = cronExpression.split("[ \t]");
if (parts.length<Parts.values().length-1 || parts.length>Parts.values().length)
throw new CronExpressionException("Expression has "+parts.length+" parts; should have 6 or 7: "+cronExpression);
if (parts.length<Parts.values().length) {
// add year wildcard
String newparts [] = new String[parts.length+1];
newparts[parts.length] = "*";
for (int i=0; i<parts.length; i++)
newparts[i] = parts[i];
parts = newparts;
}
return parts;
}
/**
* @param parts
* @return
* @throws CronExpressionException
*/
private static TreeSet[] getValues(String[] parts) throws CronExpressionException {
TreeSet values[] = new TreeSet[parts.length];
for (int pi=0; pi<parts.length; pi++) {
values[pi] = parseValues(parts[pi], Parts.values()[pi]);
if (values[pi].size()==0)
throw new CronExpressionException("Part "+Parts.values()[pi]+" has no valid value(s): "+parts[pi]);
}
return values;
}
/** return first time matching cron expression no earlier than minTime.
* Supports number, day name, month name, range (-), repeat every (/).
* Does not support 'W', 'L', '#', 'C'.
*
* return 0 if no match.
*/
public static long getNextCronTime(String cronExpression, TreeSet values[], long minTime, long maxTime) throws CronExpressionException {
// iteratively increase minTime until all constraints are satisified or maxTime reached
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("Z"));
// next second?!
cal.setTimeInMillis(minTime);
if (cal.get(Calendar.MILLISECOND)!=0) {
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.SECOND, 1);
minTime = cal.getTimeInMillis();
}
long lastMinTime = 0;
recheck:
while(minTime<=maxTime) {
// almost a second
if (minTime<=lastMinTime+900)
throw new CronExpressionException("Evaluating "+cronExpression+" stuck at "+minTime+"/"+lastMinTime);
lastMinTime = minTime;
//logger.info("Check time "+minTime+": "+cal.get(Calendar.SECOND)+" "+cal.get(Calendar.MINUTE)+" "+cal.get(Calendar.HOUR_OF_DAY)+" "+cal.get(Calendar.DAY_OF_MONTH)+" "+cal.get(Calendar.MONTH)+" "+cal.get(Calendar.DAY_OF_WEEK)+" "+cal.get(Calendar.YEAR));
//cal.setTimeInMillis(minTime);
nextpart:
for (int pi=values.length-1; pi>=0; pi--) {
Parts part = Parts.values()[pi];
int val = part.getValue(cal);
if (!values[pi].contains(val))
{
//logger.info(part+" "+val+" not in "+values[pi]+"...");
// need to reset to 0 all smaller components of the time...
// then advance to the next value of the unit in question...
// If that will wrap the next bigger unit we also reset this unit
// then go back around the loop
switch (part) {
case YEAR:
cal.set(Calendar.MONTH, cal.getMinimum(Calendar.MONTH));
// drop through
case MONTH:
cal.set(Calendar.DAY_OF_MONTH, cal.getMinimum(Calendar.DAY_OF_MONTH));
// drop through
case DAY_OF_MONTH:
case DAY_OF_WEEK:
cal.set(Calendar.HOUR_OF_DAY, cal.getMinimum(Calendar.HOUR_OF_DAY));
// drop through
case HOURS:
cal.set(Calendar.MINUTE, cal.getMinimum(Calendar.MINUTE));
// drop through
case MINUTES:
cal.set(Calendar.SECOND, cal.getMinimum(Calendar.SECOND));
// drop through
case SECONDS:
cal.set(Calendar.MILLISECOND, cal.getMinimum(Calendar.MILLISECOND));
break;
}
// advance
SortedSet<Integer> tailSet = ((TreeSet<Integer>)values[pi]).tailSet(val+1);
if (tailSet.size()>0) {
cal.add(part.calendarIx(), tailSet.first()-val);
long newMinTime = cal.getTimeInMillis();
if (newMinTime<=minTime)
throw new CronExpressionException("Evaluating "+cronExpression+" stuck at "+minTime+"/"+newMinTime+", "+part+"="+val);
minTime = newMinTime;
//logger.info("Fail on "+part+": "+val+" vs "+values[pi]+"; advance to "+minTime);
continue recheck;
}
// wrap around
// set this part to min.
cal.set(part.calendarIx(), cal.getMinimum(part.calendarIx()));
// advance the next part
switch (part) {
case YEAR:
// years cannot wrap around
return 0;
case MONTH:
cal.add(Calendar.YEAR, 1);
break;
case DAY_OF_MONTH:
cal.add(Calendar.MONTH, 1);
break;
case DAY_OF_WEEK:
cal.add(Calendar.WEEK_OF_YEAR, 1);
break;
case HOURS:
cal.add(Calendar.DAY_OF_YEAR, 1);
break;
case MINUTES:
cal.add(Calendar.HOUR_OF_DAY, 1);
break;
case SECONDS:
cal.add(Calendar.MINUTE, 1);
break;
}
// try again...
long newMinTime = cal.getTimeInMillis();
if (newMinTime<=minTime)
throw new CronExpressionException("Evaluating "+cronExpression+" stuck at "+minTime+"/"+newMinTime+", "+part+"="+val+" (wrap-around)");
minTime = newMinTime;
//logger.info("Fail on "+part+": "+val+" vs "+values[pi]+"; advance/wrap to "+minTime);
continue recheck;
}
else {
//logger.info("ok: "+part+" "+val+" in "+values[pi]);
}
}
//logger.info("Match "+minTime);
// got one!
return minTime;
}
// no deal
return 0;
}
/** get game time options for cron / time
* @throws CronExpressionException */
public static GameTimeOptions getGameTimeOptions(String cronExpression, long minTime) throws CronExpressionException {
GameTimeOptions gto = new GameTimeOptions();
// parse expression
String parts[] = getParts(cronExpression);
TreeSet values[] = getValues(parts);
// iteratively increase minTime until all constraints are satisified or maxTime reached
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("Z"));
// next second?!
cal.setTimeInMillis(minTime);
gto.setDayOfMonth(getGameTimeOption(cal, values, Parts.DAY_OF_MONTH));
gto.setDayOfWeek(getGameTimeOption(cal, values, Parts.DAY_OF_WEEK));
gto.setHour(getGameTimeOption(cal, values, Parts.HOURS));
gto.setMinute(getGameTimeOption(cal, values, Parts.MINUTES));
gto.setMonth(getGameTimeOption(cal, values, Parts.MONTH));
gto.setSecond(getGameTimeOption(cal, values, Parts.SECONDS));
gto.setYear(getGameTimeOption(cal, values, Parts.YEAR));
return gto;
}
/**
* @param cal
* @param values
* @param dayOfMonth
* @return
*/
private static GameTimeOption getGameTimeOption(Calendar cal, TreeSet values[], Parts part) {
GameTimeOption gto = new GameTimeOption();
gto.setInitialValue(part.getValue(cal));
int vs[] = new int[values[part.ordinal()].size()];
Iterator<Integer> vi = values[part.ordinal()].iterator();
for (int i=0; i<vs.length && vi.hasNext(); i++)
vs[i] = vi.next();
gto.setOptions(vs);
return gto;
}
@SuppressWarnings("unchecked")
public static String getTimeOptionsJson(String cronExpression) throws CronExpressionException {
// parse expression
String parts[] = getParts(cronExpression);
TreeSet values[] = getValues(parts);
StringWriter sw = new StringWriter();
JSONWriter jw = new JSONWriter(sw);
try {
jw.array();
for (int i=0; i<values.length; i++) {
jw.array();
for (Object o : values[i])
jw.value(o);
jw.endArray();
}
jw.endArray();
} catch (JSONException e) {
logger.warning("getTimeOptionsJson("+cronExpression+"): "+e);
throw new CronExpressionException("getTimeOptionsJson("+cronExpression+"): "+e);
}
return sw.toString();
}
/** get TreeSet[] of values for use with getNextCronTime
* @throws org.json.JSONException */
public static TreeSet[] parseTimeOptionsJson(String timeOptionsJson) throws org.json.JSONException {
JSONArray array = new JSONArray(timeOptionsJson);
TreeSet values[] = new TreeSet[array.length()];
for (int i=0; i<array.length(); i++) {
TreeSet<Integer> ts = new TreeSet<Integer>();
values[i] = ts;
JSONArray va = array.getJSONArray(i);
for (int j=0; j<va.length(); j++)
ts.add(va.getInt(j));
}
return values;
}
}