454 lines
15 KiB
Java
454 lines
15 KiB
Java
/*
|
|
* Twidere - Twitter client for Android
|
|
*
|
|
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
|
|
*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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
|
|
* <p>
|
|
* 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 <T> 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<String> 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()]);
|
|
}
|
|
|
|
}
|