content: /**
   * 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.apache.aurora.scheduler.cron;
  
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Optional;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  import com.google.common.annotations.VisibleForTesting;
  import com.google.common.base.Splitter;
  import com.google.common.collect.BiMap;
  import com.google.common.collect.ContiguousSet;
  import com.google.common.collect.DiscreteDomain;
  import com.google.common.collect.ImmutableBiMap;
  import com.google.common.collect.ImmutableRangeSet;
  import com.google.common.collect.Lists;
  import com.google.common.collect.Range;
  import com.google.common.collect.RangeSet;
  import com.google.common.collect.TreeRangeSet;
  
  import static java.util.Objects.requireNonNull;
  
  import static com.google.common.base.Preconditions.checkArgument;
  
  /**
   * A pattern that describes one or more cron 5-tuples (minute, hour, dayOfMonth, month, dayOfWeek).
   *
   * CrontabEntries are immutable and thread-safe. Unless otherwise specified any public methods will
   * throw {@link java.lang.NullPointerException} if given a {@code null} parameter.
   *
   * The quickest way to create a {@code CrontabEntry} is to use {@link #parse(String)} or
   * {@link #tryParse(String)}.
   */
  public final class CrontabEntry {
    private static final Range<Integer> MINUTE =
        Range.closed(0, 59).canonical(DiscreteDomain.integers());
    private static final Range<Integer> HOUR =
        Range.closed(0, 23).canonical(DiscreteDomain.integers());
    private static final Range<Integer> DAY_OF_MONTH =
        Range.closed(1, 31).canonical(DiscreteDomain.integers());
    private static final Range<Integer> MONTH =
        Range.closed(1, 12).canonical(DiscreteDomain.integers());
    // NOTE: Unlike FreeBSD we don't allow "7" to mean Sunday.
    private static final Range<Integer> DAY_OF_WEEK =
        Range.closed(0, 6).canonical(DiscreteDomain.integers());
  
    private final RangeSet<Integer> minute;
    private final RangeSet<Integer> hour;
    private final RangeSet<Integer> dayOfMonth;
    private final RangeSet<Integer> month;
    private final RangeSet<Integer> dayOfWeek;
  
    private CrontabEntry(
        RangeSet<Integer> minute,
        RangeSet<Integer> hour,
        RangeSet<Integer> dayOfMonth,
        RangeSet<Integer> month,
        RangeSet<Integer> dayOfWeek) {
  
      checkEnclosed("minute", MINUTE, minute);
      checkEnclosed("hour", HOUR, hour);
      checkEnclosed("dayOfMonth", DAY_OF_MONTH, dayOfMonth);
      checkEnclosed("month", MONTH, month);
      checkEnclosed("dayOfWeek", DAY_OF_WEEK, dayOfWeek);
  
      this.minute = ImmutableRangeSet.copyOf(minute);
      this.hour = ImmutableRangeSet.copyOf(hour);
      this.dayOfMonth = ImmutableRangeSet.copyOf(dayOfMonth);
      this.month = ImmutableRangeSet.copyOf(month);
      this.dayOfWeek = ImmutableRangeSet.copyOf(dayOfWeek);
  
      checkArgument(hasWildcardDayOfMonth() || hasWildcardDayOfWeek(),
          "Specifying both dayOfWeek and dayOfMonth is not supported.");
    }
  
    private static void checkEnclosed(
        String fieldName,
        Range<Integer> fieldEnclosure,
        RangeSet<Integer> field) {
  
      checkArgument(fieldEnclosure.encloses(field.span()),
          String.format(
              "Bad specification for field %s: span(%s) = %s is not enclosed by boundary %s.",
              fieldName,
              field,
              field.span(),
              fieldEnclosure));
    }
  
    /**
     * Create a new {@link CrontabEntry} from a crontab(5)-style schedule.
     *
     * The acceptable format of {@code schedule} is mostly compatible with FreeBSD's crontab(5)
     * format, excluding "extensions" like "@every_minute."
     *
     * A crontab(5) entry consists of 5 fields (minute, hour, dayOfMonth, month, dayOfWeek) and for
     * each field supports singletons ("50"), wildcards ("*"), ranges ("1-50", "MON-SAT"), and
     * "skips" ("1-50/2", "&#42;/2").
     *
     * See http://www.freebsd.org/cgi/man.cgi?query=crontab&sektion=5 for full syntax examples.
     *
     * NOTE: While entries such as "Thursdays that fall on the 15th day of the month" are expressible
     * in the original BSD syntax, this parser rejects them with {@link IllegalArgumentException}.
     *
     * @param schedule The crontab entry to parse.
     * @return A new entry if parsing was successful.
     * @throws IllegalArgumentException if parsing failed for any reason.
     */
    public static CrontabEntry parse(String schedule) throws IllegalArgumentException {
      return new Parser(schedule).get();
    }
  
    /**
     * Create a new {@link CrontabEntry} from a crontab(5)-style schedule.
     *
     * @see #parse(String)
     * @param schedule The crontab entry to parse.
     * @return A new entry if parsing was successful, absent otherwise.
     */
    public static Optional<CrontabEntry> tryParse(String schedule) {
      try {
        return Optional.of(parse(schedule));
      } catch (IllegalArgumentException e) {
        return Optional.empty();
      }
    }
  
    private static CrontabEntry from(
        RangeSet<Integer> minute,
        RangeSet<Integer> hour,
        RangeSet<Integer> dayOfMonth,
        RangeSet<Integer> month,
        RangeSet<Integer> dayOfWeek) throws IllegalArgumentException {
  
      return new CrontabEntry(minute, hour, dayOfMonth, month, dayOfWeek);
    }
  
    private RangeSet<Integer> getMinute() {
      return minute;
    }
  
    private RangeSet<Integer> getHour() {
      return hour;
    }
  
    private RangeSet<Integer> getDayOfMonth() {
      return dayOfMonth;
    }
  
    private RangeSet<Integer> getMonth() {
      return month;
    }
  
    /**
     * The days of the week this entry matches. 0 is Sun and 6 is Sat.
     *
     * @return An immutable view of the days of the week this entry matches within [0,7).
     */
    public RangeSet<Integer> getDayOfWeek() {
      return dayOfWeek;
    }
  
    @VisibleForTesting
    boolean hasWildcardMinute() {
      return getMinute().encloses(MINUTE);
    }
  
    @VisibleForTesting
    boolean hasWildcardHour() {
      return getHour().encloses(HOUR);
    }
  
    /**
     * True if this entry covers all possible days of the month.
     */
    public boolean hasWildcardDayOfMonth() {
      return getDayOfMonth().encloses(DAY_OF_MONTH);
    }
  
    @VisibleForTesting
    boolean hasWildcardMonth() {
      return getMonth().encloses(MONTH);
    }
  
    /**
     * True if this entry covers all possible days of the week.
     */
    public boolean hasWildcardDayOfWeek() {
      return getDayOfWeek().encloses(DAY_OF_WEEK);
    }
  
    private String fieldToString(RangeSet<Integer> rangeSet, Range<Integer> coveringRange) {
      if (rangeSet.asRanges().size() == 1 && rangeSet.encloses(coveringRange)) {
        return "*";
      }
      List<String> components = Lists.newArrayList();
      for (Range<Integer> range : rangeSet.asRanges()) {
        ContiguousSet<Integer> set = ContiguousSet.create(range, DiscreteDomain.integers());
        if (set.size() == 1) {
          components.add(set.first().toString());
        } else {
          components.add(set.first() + "-" + set.last());
        }
      }
      return String.join(",", components);
    }
  
    /**
     * The minute component, in canonical form.
     */
    public String getMinuteAsString() {
      return fieldToString(getMinute(), MINUTE);
    }
  
    /**
     * The hour component, in canonical form.
     */
    public String getHourAsString() {
      return fieldToString(getHour(), HOUR);
    }
  
    /**
     * The dayOfMonth component, in canonical form.
     */
    public String getDayOfMonthAsString() {
      return fieldToString(getDayOfMonth(), DAY_OF_MONTH);
    }
  
    /**
     * The month component, in canonical form.
     */
    public String getMonthAsString() {
      return fieldToString(getMonth(), MONTH);
    }
  
    /**
     * The dayOfWeek component, in canonical form.
     */
    public String getDayOfWeekAsString() {
      return fieldToString(getDayOfWeek(), DAY_OF_WEEK);
    }
  
    /**
     * Returns a parsable string representation schedule such that
     * {@code c.equals(CrontabEntry.parse(c.toString())}.
     */
    @Override
    public String toString() {
      return String.join(
          " ",
          getMinuteAsString(),
          getHourAsString(),
          getDayOfMonthAsString(),
          getMonthAsString(),
          getDayOfWeekAsString());
    }
  
    /**
     * True when both sides would match the same set of instants.
     */
    @Override
    public boolean equals(Object o) {
      if (!(o instanceof CrontabEntry)) {
        return false;
      }
      CrontabEntry that = (CrontabEntry) o;
      return Objects.equals(getMinute(), that.getMinute())
          && Objects.equals(getHour(), that.getHour())
          && Objects.equals(getDayOfMonth(), that.getDayOfMonth())
          && Objects.equals(getMonth(), that.getMonth())
          && Objects.equals(getDayOfWeek(), that.getDayOfWeek());
    }
  
    @Override
    public int hashCode() {
      return Objects.hash(getMinute(), getHour(), getDayOfWeek(), getMonth(), getDayOfMonth());
    }
  
    private static class Parser {
      private static final Pattern CRONTAB_ENTRY = Pattern.compile(
          "^(?<minute>\\S+)"
              + "\\s+(?<hour>\\S+)"
              + "\\s+(?<dayOfMonth>\\S+)"
              + "\\s+(?<month>\\S+)"
              + "\\s+(?<dayOfWeek>\\S+)$"
      );
  
      // A single time like "5", "10", "50".
      private static final Pattern NUMBER = Pattern.compile("^(?<number>\\d+)$");
      // A wildcard ("*").
      private static final Pattern WILDCARD = Pattern.compile("^\\*$");
      // A range like "1-2", "5-10", "14-50".
      private static final Pattern RANGE = Pattern.compile("^(?<lower>\\d+)-(?<upper>\\d+)$");
      // A wildcard with a "skip" like "*/5", "*/10"
      private static final Pattern WILDCARD_WITH_SKIP = Pattern.compile("^\\*/(?<skip>\\d+)$");
      // A range with a "skip" like "1-2/2", "0-59/5"
      private static final Pattern RANGE_WITH_SKIP =
          Pattern.compile("^(?<lower>\\d+)-(?<upper>\\d+)/(?<skip>\\d+)$");
  
      private static final BiMap<String, Integer> MONTH_NAMES = ImmutableBiMap
          .<String, Integer>builder()
          .put("JAN", 1)
          .put("FEB", 2)
          .put("MAR", 3)
          .put("APR", 4)
          .put("MAY", 5)
          .put("JUN", 6)
          .put("JUL", 7)
          .put("AUG", 8)
          .put("SEP", 9)
          .put("OCT", 10)
          .put("NOV", 11)
          .put("DEC", 12)
          .build();
  
      // NOTE: Unlike FreeBSD we don't allow "7" to mean Sunday.
      private static final BiMap<String, Integer> DAY_NAMES = ImmutableBiMap
          .<String, Integer>builder()
          .put("SUN", 0)
          .put("MON", 1)
          .put("TUE", 2)
          .put("WED", 3)
          .put("THU", 4)
          .put("FRI", 5)
          .put("SAT", 6)
          .build();
  
      private final String rawMinute;
      private final String rawHour;
      private final String rawDayOfMonth;
      private final String rawMonth;
      private final String rawDayOfWeek;
  
      Parser(String schedule) throws IllegalArgumentException {
        Matcher matcher = CRONTAB_ENTRY.matcher(schedule);
        checkArgument(matcher.matches(), "Invalid cron schedule %s", schedule);
  
        rawMinute = requireNonNull(matcher.group("minute"));
        rawHour = requireNonNull(matcher.group("hour"));
        rawDayOfMonth = requireNonNull(matcher.group("dayOfMonth"));
        rawMonth = requireNonNull(matcher.group("month"));
        rawDayOfWeek = requireNonNull(matcher.group("dayOfWeek"));
      }
  
      CrontabEntry get() throws IllegalArgumentException {
        return CrontabEntry.from(
            parseMinute(),
            parseHour(),
            parseDayOfMonth(),
            parseMonth(),
            parseDayOfWeek());
      }
  
      private List<String> getComponents(String rawField) {
        return Splitter.on(",").omitEmptyStrings().splitToList(rawField);
      }
  
      private String replaceNameAliases(String rawComponent, Map<String, Integer> names) {
        String component = rawComponent.toUpperCase(Locale.ENGLISH);
        for (Map.Entry<String, Integer> entry : names.entrySet()) {
          if (component.contains(entry.getKey())) {
            component = component.replaceAll(entry.getKey(), entry.getValue().toString());
          }
        }
        return component;
      }
  
      private static RangeSet<Integer> parseComponent(
          final Range<Integer> enclosure,
          String rawComponent) throws IllegalArgumentException {
  
        if (WILDCARD.matcher(rawComponent).matches()) {
          return ImmutableRangeSet.of(enclosure);
        }
  
        Matcher matcher = NUMBER.matcher(rawComponent);
        if (matcher.matches()) {
          int number = Integer.parseInt(matcher.group("number"));
          Range<Integer> range = Range.singleton(number).canonical(DiscreteDomain.integers());
  
          checkArgument(enclosure.encloses(range), "%s does not enclose %s", enclosure, range);
  
          return ImmutableRangeSet.of(range);
        }
  
        matcher = RANGE.matcher(rawComponent);
        if (matcher.matches()) {
          int lower = Integer.parseInt(matcher.group("lower"));
          int upper = Integer.parseInt(matcher.group("upper"));
          Range<Integer> range = Range.closed(lower, upper).canonical(DiscreteDomain.integers());
  
          checkArgument(enclosure.encloses(range), "%s does not enclose %s", enclosure, range);
  
          return ImmutableRangeSet.of(range);
        }
  
        matcher = WILDCARD_WITH_SKIP.matcher(rawComponent);
        if (matcher.matches()) {
          int skip = Integer.parseInt(matcher.group("skip"));
          int start = enclosure.lowerEndpoint();
  
          checkArgument(skip > 0);
  
          ImmutableRangeSet.Builder<Integer> rangeSet = ImmutableRangeSet.builder();
          for (int i = start;  enclosure.contains(i); i += skip) {
            rangeSet.add(Range.singleton(i).canonical(DiscreteDomain.integers()));
          }
          return rangeSet.build();
        }
  
        matcher = RANGE_WITH_SKIP.matcher(rawComponent);
        if (matcher.matches()) {
          final int lower = Integer.parseInt(matcher.group("lower"));
          final int upper = Integer.parseInt(matcher.group("upper"));
          final int skip = Integer.parseInt(matcher.group("skip"));
          Range<Integer> range = Range.closed(lower, upper).canonical(DiscreteDomain.integers());
  
          checkArgument(enclosure.encloses(range), "%s does not enclose %s", enclosure, range);
          checkArgument(skip > 0, "skip value %s must be >0", skip);
          checkArgument(skip < upper, "skip value %s must be smaller than %s", skip, upper);
  
          ImmutableRangeSet.Builder<Integer> rangeSet = ImmutableRangeSet.builder();
          for (int i = lower; range.contains(i); i += skip) {
            rangeSet.add(Range.singleton(i).canonical(DiscreteDomain.integers()));
          }
          return rangeSet.build();
        }
  
        throw new IllegalArgumentException(
            "Cron schedule component " + rawComponent + " does not match any known patterns.");
      }
  
      private RangeSet<Integer> parseMinute() {
        RangeSet<Integer> minutes = TreeRangeSet.create();
        for (String component : getComponents(rawMinute)) {
          minutes.addAll(parseComponent(MINUTE, component));
        }
        return ImmutableRangeSet.copyOf(minutes);
      }
  
      private RangeSet<Integer> parseHour() {
        RangeSet<Integer> hours = TreeRangeSet.create();
        for (String component : getComponents(rawHour)) {
          hours.addAll(parseComponent(HOUR, component));
        }
        return ImmutableRangeSet.copyOf(hours);
      }
  
      private RangeSet<Integer> parseDayOfWeek() {
        RangeSet<Integer> daysOfWeek = TreeRangeSet.create();
        for (String component : getComponents(rawDayOfWeek)) {
          daysOfWeek.addAll(parseComponent(DAY_OF_WEEK, replaceNameAliases(component, DAY_NAMES)));
        }
        return ImmutableRangeSet.copyOf(daysOfWeek);
      }
  
      private RangeSet<Integer> parseMonth() {
        RangeSet<Integer> months = TreeRangeSet.create();
        for (String component : getComponents(rawMonth)) {
          months.addAll(parseComponent(MONTH, replaceNameAliases(component, MONTH_NAMES)));
        }
        return ImmutableRangeSet.copyOf(months);
      }
  
      private RangeSet<Integer> parseDayOfMonth() {
        RangeSet<Integer> daysOfMonth = TreeRangeSet.create();
        for (String component : getComponents(rawDayOfMonth)) {
          daysOfMonth.addAll(parseComponent(DAY_OF_MONTH, component));
        }
        return ImmutableRangeSet.copyOf(daysOfMonth);
      }
    }
  }
  