/**
   * Reads latest config from {@link ConfigService} for the given {@link Task} and caches it in that
   * {@link Task}. If config is found but can't be parsed, the {@link Task}'s {@code configured*}
   * fields will include the bad {@link Trigger} expression and a {@link BadConfigTrigger}. If
   * config simply can't be read at all, e.g. network outage, that will just be logged and the
   * {@link Task}'s config will be unchanged.
   *
   * <p>This also handles initialization of the {@link Task's} default {@link Trigger} <em>every
   * time</em>. Which means you can technically change the default and it will be picked up.
   *
   * @param task
   * @return
   * @throws BadTriggerConfigException if the default configuration for the given {@link Task} is
   *     broken. Exceptions during read of latest config from {@link ConfigService} are caught and
   *     handled.
   */
  protected void mergeLatestTriggerConfig(Task task) throws BadTriggerConfigException {

    // Null will mean "have no idea what the latest config would be or
    // if it even exists." Everything else will mean "either found good
    // config, use it, or found bad config and it's up to you what to do
    // with it."
    Pair<String, Trigger> newTriggerConfig = null;
    try {
      newTriggerConfig = readNewTriggerConfig(task.triggerExpressionConfigName);
    } catch (BadTriggerConfigException e) {
      LOGGER.warn(
          "Unable to parse task [{}] trigger config named [{}].",
          new Object[] {task.id, task.triggerExpressionConfigName, e});
      newTriggerConfig = new Pair<String, Trigger>(null, new BadConfigTrigger(e.getConfig(), e));
    } catch (RuntimeException e) {
      // Probably db connection or other systemic issue issue. Will end up
      // leaving this task alone.
      LOGGER.error(
          "Unable to read trigger config named [{}]. This"
              + " was probably a transient and/or systemic issue "
              + " rather than a parse issue.",
          task.triggerExpressionConfigName,
          e);
    }

    if (newTriggerConfig != null && newTriggerConfig.getSecond() == null) {
      // Really a programmer error but let's just quietly normalize to
      // simplify boolean expressions below.
      newTriggerConfig = null;
    }

    if (newTriggerConfig == null) {
      task.configuredTriggerExpression = null;
      task.configuredTrigger = null;
    } else {
      task.configuredTriggerExpression = newTriggerConfig.getFirst();
      task.configuredTrigger = newTriggerConfig.getSecond();
    }

    // Do this every time to avoid weird stuff from mis-use where the
    // expression and trigger don't match.
    task.defaultTrigger = parseTriggerConfig(task.defaultTriggerExpression);
  }
  protected Trigger parseTriggerConfig(String configValue) throws BadTriggerConfigException {

    BadTriggerConfigException badPeriodicTiggerException = null;
    BadTriggerConfigException badCronTiggerException = null;
    try {
      return tryExpressionAsPeriodicTrigger(configValue);
    } catch (BadTriggerConfigException e) {
      badPeriodicTiggerException = e;
    }

    try {
      return tryExpressionAsCronTrigger(configValue);
    } catch (BadTriggerConfigException e) {
      badCronTiggerException = e;
    }

    throw new BadTriggerConfigException(
        "All trigger config parsing attempts failed. First reason: ["
            + badPeriodicTiggerException.getMessage()
            + "]. Second reason: ["
            + badCronTiggerException.getMessage()
            + "]",
        configValue);
  }
  protected Trigger tryExpressionAsPeriodicTrigger(String configValue)
      throws BadTriggerConfigException {
    BadTriggerConfigException longParseException = null;
    try {
      final long period = Long.parseLong(configValue);
      if (period < 0) {
        return new DisabledTrigger();
      }
      // millis since that's what @Scheduled methods were historically
      // configured in
      return new PeriodicTrigger(period, TimeUnit.MILLISECONDS);
    } catch (NumberFormatException e) {
      longParseException =
          new BadTriggerConfigException(
              "Config [" + configValue + "] did not parse to a long.", configValue, e);
    } catch (IllegalArgumentException e) {
      longParseException =
          new BadTriggerConfigException(
              "Config [" + configValue + "] could not be used to initialize a PeriodicTrigger",
              configValue,
              e);
    }

    final Matcher matcher = PERIODIC_TRIGGER_WITH_INITIAL_DELAY_PATTERN.matcher(configValue);
    if (!(matcher.matches())) {
      throw new BadTriggerConfigException(
          "Trigger expression could not be parsed as either a"
              + " simple period or as a period with an initial "
              + "offset. Original parse failure: ["
              + longParseException.getMessage()
              + "]. To be considered a period with an offset, "
              + "the expression must match this regexp"
              + "(without brackets): ["
              + PERIODIC_TRIGGER_WITH_INITIAL_DELAY_PATTERN
              + "]",
          configValue);
    }

    try {
      final String periodStr = matcher.group(1);
      final long periodLong = Long.parseLong(periodStr);

      if (periodLong < 0) {
        return new DisabledTrigger();
      }

      final String offsetStr = matcher.group(2);
      final long offsetLong = Long.parseLong(offsetStr);

      final PeriodicTrigger trigger = new PeriodicTrigger(periodLong, TimeUnit.MILLISECONDS);
      trigger.setInitialDelay(offsetLong);
      return trigger;
    } catch (NumberFormatException e) {
      throw new BadTriggerConfigException(
          "Trigger expression could not be parsed as either a"
              + " simple period or as a period with an initial "
              + "offset. Original parse failure: ["
              + longParseException.getMessage()
              + "]. To be considered a period with an initial "
              + "delay, the expression must match this regexp"
              + "(without brackets): ["
              + PERIODIC_TRIGGER_WITH_INITIAL_DELAY_PATTERN
              + "]",
          configValue,
          e);
    } catch (IllegalArgumentException e) {
      throw new BadTriggerConfigException(
          "Trigger expression parsed as a period [{}] with an "
              + "initial delay [{}] but could not be used to "
              + "initialize a PeriodicTrigger",
          configValue,
          e);
    }
  }