All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.pekko.extension.quartz.QuartzSchedules.scala Maven / Gradle / Ivy

The newest version!
package org.apache.pekko.extension.quartz

import com.typesafe.config.{ Config, ConfigException, ConfigObject }
import java.util.TimeZone
import java.util.Date
import scala.util.control.Exception._
import org.quartz._
import collection.immutable
import java.text.ParseException

import scala.collection.JavaConverters._

/**
 * This is really about triggers - as the "job" is roughly defined in the code that refers to the trigger.
 *
 * I call them Schedules to get people not thinking about Quartz in Quartz terms (mutable jobs, persistent state)
 *
 * All jobs "start" immediately.
 */
object QuartzSchedules {
  // timezone (parseable) [optional, defaults to UTC]
  // calendars = list of calendar names that "modify" this schedule
  // description = an optional description of the job [string] [optional]
  // expression = cron expression complying to Quartz' Cron Expression rules.
  // TODO - Misfire Handling

  val catchMissing = catching(classOf[ConfigException.Missing])
  val catchWrongType = catching(classOf[ConfigException.WrongType])
  val catchParseErr = catching(classOf[ParseException])

  def apply(config: Config, defaultTimezone: TimeZone): immutable.Map[String, QuartzSchedule] = catchMissing
    .opt {

      /** The extra toMap call is because the asScala gives us a mutable map... */
      config.getConfig("schedules").root.asScala.toMap.flatMap {
        case (key, value: ConfigObject) =>
          Some(key -> parseSchedule(key, value.toConfig, defaultTimezone))
        case _ =>
          None
      }
    }
    .getOrElse(immutable.Map.empty[String, QuartzSchedule])

  def parseSchedule(name: String, config: Config, defaultTimezone: TimeZone): QuartzSchedule = {
    // parse common attributes
    val timezone = catchMissing
      .opt {
        TimeZone.getTimeZone(
          config.getString("timezone")
        ) // todo - this is bad, as Java silently swaps the timezone if it doesn't match...
      }
      .getOrElse(defaultTimezone)

    val calendar = catchMissing.opt {
      Option(
        config.getString("calendar")
      ) // TODO - does Quartz validate for us that a calendar referenced is valid/invalid?
    }.flatten

    val desc = catchMissing.opt {
      config.getString("description")
    }

    parseCronSchedule(name, desc, config)(timezone, calendar)
  }

  def parseCronSchedule(
      name: String,
      desc: Option[String],
      config: Config
  )(tz: TimeZone, calendar: Option[String]): QuartzCronSchedule = {
    val expression = catchMissing.or(catchWrongType).either { config.getString("expression") } match {
      case Left(t) =>
        throw new IllegalArgumentException(
          "Invalid or Missing Configuration entry 'expression' for Cron Schedule '%s'. You must provide a valid Quartz CronExpression."
            .format(name),
          t
        )
      case Right(str) =>
        catchParseErr.either(new CronExpression(str)) match {
          case Left(t) =>
            throw new IllegalArgumentException(
              "Invalid 'expression' for Cron Schedule '%s'. Failed to validate CronExpression.".format(name),
              t
            )
          case Right(expr) => expr
        }
    }
    new QuartzCronSchedule(name, desc, expression, tz, calendar)
  }
}

sealed trait QuartzSchedule {
  type T <: Trigger

  def name: String

  def description: Option[String]

  // todo - I don't like this as we can't guarantee the builder's state, but the Quartz API forces our hand
  def schedule: ScheduleBuilder[T]

  // The name of the optional exclusion calendar to use.
  // NOTE: This formerly was "calendars" but that functionality has since been removed as Quartz never supported more
  // than one calendar anyways.
  def calendar: Option[String]

  /**
   * Utility method that builds a trigger with the data this schedule contains, given a name. Job association can happen
   * separately at schedule time.
   *
   * @param name
   *   The name of the job / schedule.
   * @param futureDate
   *   The Optional earliest date at which the job may fire.
   * @return
   *   The new trigger instance.
   */
  def buildTrigger(name: String, futureDate: Option[Date] = None): T = {
    val partialTriggerBuilder = TriggerBuilder
      .newTrigger()
      .withIdentity(name + "_Trigger")
      .withDescription(description.orNull)
      .withSchedule(schedule)

    var triggerBuilder = futureDate match {
      case Some(fd) => partialTriggerBuilder.startAt(fd)
      case None     => partialTriggerBuilder.startNow()
    }

    triggerBuilder = calendar.map(triggerBuilder.modifiedByCalendar).getOrElse(triggerBuilder)
    triggerBuilder.build()
  }

}

final class QuartzCronSchedule(
    val name: String,
    val description: Option[String] = None,
    val expression: CronExpression,
    val timezone: TimeZone,
    val calendar: Option[String] = None
) extends QuartzSchedule {

  type T = CronTrigger

  // Do *NOT* build, we need the uncompleted builder. I hate the Quartz API, truly.
  val schedule: CronScheduleBuilder = CronScheduleBuilder.cronSchedule(expression).inTimeZone(timezone)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy