/* * Copyright 2009 NCHOVY * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.krakenapps.cron.impl; import java.text.ParseException; import java.util.BitSet; import java.util.Calendar; /** * component of Schedule. represents a field of the schedule. * * @author periphery * @since 1.0.0 */ public final class CronField { private final Type type; // bitmap index 0 represents 1 for CronField.type Month and dayOfMonth. private final BitSet bits; private final String exp; public enum Type { MINUTE("Minute", 60, 0), HOUR("Hour", 24, 0), DAY_OF_MONTH("DayOfMonth", 31, 1), MONTH("Month", 12, 1), DAY_OF_WEEK("DayOfWeek", 7, 0); private final String fieldName; // name of the type. private final int bitLength; private final int base; private Type(String fieldName, int bitLength, int base) { this.fieldName = fieldName; this.bitLength = bitLength; this.base = base; } public String toString() { return this.fieldName; } private int getBitLength() { return this.bitLength; } private int getBase() { return this.base; } private int getLast() { return this.getBase() + this.getBitLength() - 1; } /** * return true if the num is out of range of given type. valid range of * num: day of week (0 - 6) (Sunday=0) month (1 - 12) day of month (1 - * 31) hour (0 - 23) min (0 - 59) */ private boolean invalid(int num) { return num < this.getBase() || num > this.getLast(); } private void setBits(BitSet bits, int num) { if (this.base == 0) bits.set(num); else bits.set(num - 1); } /** * initialize to all_false if type is day of week. */ private void setBits(BitSet bits) { if (this != DAY_OF_WEEK) bits.set(0, this.bitLength); } private boolean isAllTrue(BitSet bits) { return bits.nextClearBit(0) >= this.bitLength; } private boolean isAllFalse(BitSet bits) { return (bits.nextSetBit(0) == -1); } } public CronField(Type type, String exp) throws ParseException { this.type = type; this.bits = new BitSet(this.type.bitLength); this.exp = parseExp(this.type, this.bits, exp); } /** * parse expression and set bitmap. if the expression is null, it is same as * expression "*"(wild card) */ private static String parseExp(CronField.Type type, BitSet bits, String exp) throws ParseException { // filter "*" or null. if (exp == null || exp.matches("[*]")) { type.setBits(bits); return "*"; // filter empty string. throw Exception. } else if (exp.length() == 0) { throw new ParseException("cron field cannot be set with an empty string : " + exp, 0); // filter list type. } else if (exp.matches("([0-9]+[,])+[0-9]+")) { String[] splited = exp.split("[,]"); for (String sp : splited) { parseExp(type, bits, sp);// recursive call } // filter interval type } else if (exp.matches("[*]/[0-9]+")) { try { int interval = Integer.parseInt(exp.split("[/]")[1]); for (int i = type.getBase(); !type.invalid(i); i += interval) { type.setBits(bits, i); } } catch (Exception e) { throw new ParseException("wrong interval format. e.g. 2/* = every 2nd : " + exp, 0); } // filter range type } else if (exp.matches("[0-9]+[-][0-9]+")) { int from = Integer.parseInt(exp.split("[-]")[0]); int to = Integer.parseInt(exp.split("[-]")[1]); if (to < from || type.invalid(to) || type.invalid(from)) throw new ParseException("invalida range. e.g. 2-5 = 2 to 5 : " + exp, 0); for (int i = from; !type.invalid(i) && i <= to; i++) { type.setBits(bits, i); } // filter single number } else if (exp.matches("[0-9]+")) { int num = Integer.parseInt(exp); if (type.invalid(num)) throw new ParseException("value out of range : " + exp, 0); type.setBits(bits, num); } else throw new ParseException("wrong cron field format : " + exp, 0); return exp; } /** * if day_of_week is not all_false and day_of_month is all_true, set * day_of_month to all_false. this is because it is natural to think * "0 0 * * 0" as 'weekly' not 'daily'. this is the reason why day_of_week * is initially set to all_false. * * @param dom * day of month * @param dow * day of week * @throws IllegalTypeException * when called with cronfield except for dom and dow. */ public static void solveCollision(CronField dom, CronField dow) throws IllegalTypeException { if (dom.type != Type.DAY_OF_MONTH || dow.type != Type.DAY_OF_WEEK) throw new IllegalTypeException("solveCollision should only be called with dom and dow."); if (dom.type.isAllTrue(dom.bits) && !dow.type.isAllFalse(dow.bits)) { dom.bits.clear(); } } /** * returns next matching occurrence after given start value.(including start * value itself) * * @param start * @return next matching occurrence * @throws IllegalTypeException * when called with dom or dow. should call next(Calender, * CronField) instead. */ public int next(int start) throws IllegalTypeException { if (this.type.equals(CronField.Type.DAY_OF_MONTH) || this.type.equals(CronField.Type.DAY_OF_WEEK)) throw new IllegalTypeException( "not allowed for day_of_month and day_of_week. use next(Calendar, CronField) instead."); return this.bits.nextSetBit(start); } /** * returns the first matching occurrence. * * @return first matching occurrence. * @throws IllegalTypeException * when called with dom or dow. should call first(Calender, * CronField) instead. */ public int first() throws IllegalTypeException { if (this.type.equals(CronField.Type.DAY_OF_MONTH) || this.type.equals(CronField.Type.DAY_OF_WEEK)) throw new IllegalTypeException( "not allowed for day_of_month and day_of_week. use next(Calendar, CronField) instead."); return this.next(0); } /** * returns next matching occurrence after given start value.(including start * value itself) * * @param base * @param dow * day of week * @return * @throws IllegalTypeException * when called with a cronField except for dom */ public int next(Calendar base, CronField dow) throws IllegalTypeException { if (!this.type.equals(CronField.Type.DAY_OF_MONTH)) throw new IllegalTypeException("only for day_of_month. use next(Calendar, CronField) instead."); return dow2month(base, dow).nextSetBit(base.get(Calendar.DAY_OF_MONTH) - 1); } /** * returns the first matching occurrence. * * @return first matching occurrence. * @throws IllegalTypeException * when called with a cronField except for dom */ public int first(Calendar base, CronField dow) throws IllegalTypeException { if (!this.type.equals(CronField.Type.DAY_OF_MONTH)) throw new IllegalTypeException("only for day_of_month. use next(Calendar, CronField) instead."); return dow2month(base, dow).nextSetBit(0); } // for merging day_of_month and day_of_week. // generates new bitmap. private BitSet dow2month(Calendar base, CronField dow) { Calendar clone = (Calendar) base.clone(); clone.set(Calendar.DAY_OF_MONTH, 1); int weekDayOf_1 = clone.get(Calendar.DAY_OF_WEEK) - 1; // 1 for offset // merge dow and dom bitmaps. BitSet dow2month = mergeBits(weekDayOf_1, this.bits, dow.bits); // turn off for surplus days of month. (e.g. turn off 30 and 31 from // Feb.) clone.add(Calendar.MONTH, 1); clone.add(Calendar.DAY_OF_MONTH, -1); int lastDayOfMonth = clone.get(Calendar.DAY_OF_MONTH); for (int i = lastDayOfMonth; i < this.type.bitLength; i++) dow2month.clear(i); return dow2month; } // generate 31 length bitmap by duplicating dow bitmap. private static BitSet mergeBits(int weekDayOf_1, BitSet dom, BitSet dow) { BitSet dow2month = new BitSet(); for (int i = 0; i < CronField.Type.DAY_OF_MONTH.getBitLength(); i++) { int weekDayOf_i = (i + weekDayOf_1) % 7; dow2month.set(i, dow.get(weekDayOf_i)); } dow2month.or(dom); // dow bits and dom bits are merged by OR operation. return dow2month; } @Override public String toString() { return this.exp; } public String debugString() { return this.bits.toString(); } }