/* * Twidere - Twitter client for Android * * Copyright (C) 2012-2017 Mariotaku Lee * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.mariotaku.twidere.model; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; import java.util.List; /** * Single-file Cron expression parser *

* Supports POSIX standard syntax only */ public class CronExpression { /** * Equivalent to {@code 0 0 1 1 *} */ public static final CronExpression YEARLY = new CronExpression(new Field[]{ BasicField.zero(FieldType.MINUTE), BasicField.zero(FieldType.HOUR_OF_DAY), BasicField.one(FieldType.DAY_OF_MONTH), BasicField.one(FieldType.MONTH), AnyField.INSTANCE, null, }); public static final CronExpression ANNUALLY = YEARLY; /** * Equivalent to {@code 0 0 1 * *} */ public static final CronExpression MONTHLY = new CronExpression(new Field[]{ BasicField.zero(FieldType.MINUTE), BasicField.zero(FieldType.HOUR_OF_DAY), BasicField.one(FieldType.DAY_OF_MONTH), AnyField.INSTANCE, AnyField.INSTANCE, null, }); /** * Equivalent to {@code 0 0 * * 0} */ public static final CronExpression WEEKLY = new CronExpression(new Field[]{ BasicField.zero(FieldType.MINUTE), BasicField.zero(FieldType.HOUR_OF_DAY), AnyField.INSTANCE, AnyField.INSTANCE, BasicField.zero(FieldType.DAY_OF_WEEK), null, }); /** * Equivalent to {@code 0 0 * * *} */ public static final CronExpression DAILY = new CronExpression(new Field[]{ BasicField.zero(FieldType.MINUTE), BasicField.zero(FieldType.HOUR_OF_DAY), AnyField.INSTANCE, AnyField.INSTANCE, AnyField.INSTANCE, null, }); /** * Equivalent to {@code 0 * * * *} */ public static final CronExpression HOURLY = new CronExpression(new Field[]{ BasicField.zero(FieldType.MINUTE), AnyField.INSTANCE, AnyField.INSTANCE, AnyField.INSTANCE, AnyField.INSTANCE, null, }); private Field[] fields; private CronExpression(@NonNull Field[] fields) { if (fields.length < 5) throw new IllegalArgumentException("Fields count must >= 5"); this.fields = fields; } @NonNull public static CronExpression valueOf(@NonNull String string) throws ParseException { if (string.length() == 0) { throw new ParseException("Cron expression is empty", -1); } if (string.charAt(0) == '@') { // Parse predefined final String substr = string.substring(1); switch (substr) { case "yearly": return YEARLY; case "annually": return ANNUALLY; case "monthly": return MONTHLY; case "weekly": return WEEKLY; case "daily": return DAILY; case "hourly": return HOURLY; } throw new ParseException("Unknown pre-defined value " + substr, 1); } final String[] segments = split(string, ' '); if (segments.length > 6) { throw new ParseException("Unrecognized segments " + string, -1); } // Parse minute field Field[] fields = new Field[6]; fields[0] = FieldType.MINUTE.parseField(segments[0]); // Parse hour field fields[1] = FieldType.HOUR_OF_DAY.parseField(segments[1]); // Parse day-of-month field fields[2] = FieldType.DAY_OF_MONTH.parseField(segments[2]); // Parse month field fields[3] = FieldType.MONTH.parseField(segments[3]); // Parse day-of-week field fields[4] = FieldType.DAY_OF_WEEK.parseField(segments[4]); return new CronExpression(fields); } public boolean matches(Calendar cal) { for (Field field : fields) { if (field == null) continue; if (!field.contains(cal)) return false; } return true; } public String toExpression() { StringBuilder sb = new StringBuilder(); for (int i = 0, j = fields.length; i < j; i++) { Field field = fields[i]; if (field == null) continue; if (i != 0) { sb.append(' '); } sb.append(field.toExpression()); } return sb.toString(); } interface Field { boolean contains(Calendar cal); String toExpression(); } public enum FieldType { MINUTE(Calendar.MINUTE, 0, new Range(0, 59), null), HOUR_OF_DAY(Calendar.HOUR_OF_DAY, 0, new Range(0, 23), null), DAY_OF_MONTH(Calendar.DAY_OF_MONTH, 0, new Range(1, 31), null), MONTH(Calendar.MONTH, 1, new Range(1, 12), new String[]{"JAN", "FEB", "MAR", "APR", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}), DAY_OF_WEEK(Calendar.DAY_OF_WEEK, -1, new Range(0, 6), new String[]{"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}), /* Used in nncron, not decided whether to implement */ YEAR(Calendar.YEAR, 0, new Range(1970, 2099), null); final int calendarField; final int calendarOffset; final Range allowedRange; final String[] textRepresentations; FieldType(int calendarField, int calendarOffset, Range allowedRange, @Nullable String[] textRepresentations) { this.calendarField = calendarField; this.calendarOffset = calendarOffset; this.allowedRange = allowedRange; this.textRepresentations = textRepresentations; } Field parseField(@NonNull String text) throws ParseException { if ("*".equals(text)) return AnyField.INSTANCE; return BasicField.parse(text, this); } } enum AnyField implements Field { INSTANCE; @Override public boolean contains(Calendar cal) { return true; } @Override public String toExpression() { return "*"; } } /** * POSIX-compliant CRON field */ final static class BasicField implements Field { @NonNull final Range[] ranges; final int calendarField; final int calendarOffset; BasicField(@NonNull final Range[] ranges, int calendarField, int calendarOffset) { this.ranges = ranges; this.calendarField = calendarField; this.calendarOffset = calendarOffset; } public static Field parse(String text, FieldType fieldType) throws ParseException { final Range allowedRange = fieldType.allowedRange; final String[] rangeStrings = split(text, ','); final Range[] ranges = new Range[rangeStrings.length]; final String[] textRepresentations = fieldType.textRepresentations; for (int i = 0, l = rangeStrings.length; i < l; i++) { String rangeString = rangeStrings[i]; ranges[i] = Range.parse(rangeString, allowedRange, textRepresentations); if (!allowedRange.contains(ranges[i])) { throw new ParseException(ranges[i] + " out of range " + allowedRange, -1); } } return new BasicField(ranges, fieldType.calendarField, fieldType.calendarOffset); } public static Field zero(FieldType fieldType) { final Range[] ranges = {Range.single(0)}; return new BasicField(ranges, fieldType.calendarField, fieldType.calendarOffset); } public static Field one(FieldType fieldType) { final Range[] ranges = {Range.single(1)}; return new BasicField(ranges, fieldType.calendarField, fieldType.calendarOffset); } @Override public boolean contains(Calendar cal) { final int cmp = cal.get(calendarField) + calendarOffset; for (Range range : ranges) { if (range.contains(cmp)) return true; } return false; } @Override public String toExpression() { StringBuilder sb = new StringBuilder(); for (int i = 0, j = ranges.length; i < j; i++) { Range range = ranges[i]; if (i != 0) { sb.append(','); } sb.append(range.toExpression()); } return sb.toString(); } } final static class DayOfMonthField implements Field { static Field parse(String text, @NonNull Range allowedRange, @Nullable String[] textRepresentations) { throw new UnsupportedOperationException(); } @Override public boolean contains(Calendar cal) { throw new UnsupportedOperationException(); } @Override public String toExpression() { throw new UnsupportedOperationException(); } } final static class DayOfWeekField implements Field { public static Field parse(String text, @NonNull Range allowedRange, @Nullable String[] textRepresentations) { throw new UnsupportedOperationException(); } @Override public boolean contains(Calendar cal) { throw new UnsupportedOperationException(); } @Override public String toExpression() { throw new UnsupportedOperationException(); } } final static class Range { final int start, endInclusive; Range(int start, int endInclusive) { if (endInclusive < start) { throw new IllegalArgumentException("endInclusive < start"); } this.start = start; this.endInclusive = endInclusive; } public boolean contains(int num) { return num >= start && num <= endInclusive; } public boolean contains(Range that) { return this.start <= that.start && this.endInclusive >= that.endInclusive; } public int size() { return endInclusive - start + 1; } public int valueAt(int index) { if (index >= size()) { throw new IndexOutOfBoundsException("Range index out of bounds"); } return start + index; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Range range = (Range) o; if (start != range.start) return false; return endInclusive == range.endInclusive; } @Override public int hashCode() { int result = start; result = 31 * result + endInclusive; return result; } @Override public String toString() { return "Range{" + "start=" + start + ", endInclusive=" + endInclusive + '}'; } public static Range single(int num) { return new Range(num, num); } public static Range parse(String string, Range allowedRange, @Nullable String[] textRepresentations) throws ParseException { int dashIdx = string.indexOf('-'); if (dashIdx == -1) { return single(parseNumber(string, allowedRange, textRepresentations)); } final int start = parseNumber(string.substring(0, dashIdx), allowedRange, textRepresentations); final int endInclusive = parseNumber(string.substring(dashIdx + 1), allowedRange, textRepresentations); return new Range(start, endInclusive); } private static int parseNumber(String input, Range allowedRange, @Nullable String[] textRepresentations) throws ParseException { if (textRepresentations != null) { int textRepresentationIndex = indexOf(textRepresentations, input); if (textRepresentationIndex != -1) { return allowedRange.valueAt(textRepresentationIndex); } } try { return Integer.parseInt(input); } catch (NumberFormatException e) { throw new ParseException(e.getMessage(), -1); } } public String toExpression() { if (endInclusive == start) return Integer.toString(start); return start + "-" + endInclusive; } } private static int indexOf(@NonNull final T[] array, @NonNull final T objectToFind) { int length = array.length; for (int i = 0; i < length; i++) { if (objectToFind.equals(array[i])) { return i; } } return -1; } private static String[] split(@NonNull final String str, final char separatorChar) { // Performance tuned for 2.0 (JDK1.4) final int len = str.length(); if (len == 0) { return new String[0]; } final List list = new ArrayList<>(); int i = 0, start = 0; boolean match = false; while (i < len) { if (str.charAt(i) == separatorChar) { if (match) { list.add(str.substring(start, i)); match = false; } start = ++i; continue; } match = true; i++; } if (match) { list.add(str.substring(start, i)); } return list.toArray(new String[list.size()]); } }