Utility functions for converting frontend schedule dictionaries to cron strings and vice versa. See http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger.html#format for details on cron format. | (ns metabase.util.cron (:require [clojure.string :as str] [metabase.util.i18n :as i18n] [metabase.util.malli :as mu] [metabase.util.malli.registry :as mr] [metabase.util.malli.schema :as ms]) (:import (net.redhogs.cronparser CronExpressionDescriptor) (org.quartz CronExpression))) |
(set! *warn-on-reflection* true) | |
(mr/def ::CronScheduleString (mu/with-api-error-message [:and ms/NonBlankString [:fn {:error/fn (fn [{:keys [value]} _] (try (CronExpression/validateExpression value) (catch Throwable e (str "Invalid cron schedule string: " (.getMessage e)))))} (fn [^String s] (try (CronExpression/validateExpression s) true (catch Throwable _ false)))]] (i18n/deferred-tru "value must be a valid Quartz cron schedule string."))) | |
Malli Schema for a valid cron schedule string. | (def CronScheduleString [:ref ::CronScheduleString]) |
(mr/def ::CronHour [:int {:min 0, :max 23}]) | |
(mr/def ::CronMinute [:int {:min 0, :max 59}]) | |
(mr/def ::ScheduleMap (mu/with-api-error-message [:map {:error/message "Expanded schedule map"} [:schedule_type [:enum "hourly" "daily" "weekly" "monthly"]] [:schedule_day {:optional true} [:maybe [:enum "sun" "mon" "tue" "wed" "thu" "fri" "sat"]]] [:schedule_frame {:optional true} [:maybe [:enum "first" "mid" "last"]]] [:schedule_hour {:optional true} [:maybe ::CronHour]] [:schedule_minute {:optional true} [:maybe ::CronMinute]]] (i18n/deferred-tru "value must be a valid schedule map. See schema in metabase.util.cron for details."))) | |
Schema for a frontend-parsable schedule map. Used for Pulses and DB scheduling. | (def ScheduleMap [:ref ::ScheduleMap]) |
+----------------------------------------------------------------------------------------------------------------+ | SCHEDULE MAP -> CRON STRING | +----------------------------------------------------------------------------------------------------------------+ | |
(mu/defn- cron-string :- CronScheduleString "Build a cron string from key-value pair parts." [{:keys [seconds minutes hours day-of-month month day-of-week year]}] (str/join " " [(or seconds "0") (or minutes "0") (or hours "*") (or day-of-month "*") (or month "*") (or day-of-week "?") (or year "*")])) | |
(def ^:private day-of-week->cron {"sun" 1 "mon" 2 "tue" 3 "wed" 4 "thu" 5 "fri" 6 "sat" 7}) | |
(defn- frame->cron [frame day-of-week] (if day-of-week ;; specific days of week like Mon or Fri (assoc {:day-of-month "?"} :day-of-week (case frame "first" (str (day-of-week->cron day-of-week) "#1") "last" (str (day-of-week->cron day-of-week) "L"))) ;; specific CALENDAR DAYS like 1st or 15th (assoc {:day-of-week "?"} :day-of-month (case frame "first" "1" "mid" "15" "last" "L")))) | |
(mu/defn schedule-map->cron-string :- CronScheduleString "Convert the frontend schedule map into a cron string." [{day-of-week :schedule_day, hour :schedule_hour, minute :schedule_minute, frame :schedule_frame, schedule-type :schedule_type} :- ScheduleMap] (cron-string (case (keyword schedule-type) :hourly {:minutes minute} :daily {:hours (or hour 0)} :weekly {:hours hour :day-of-week (day-of-week->cron day-of-week) :day-of-month "?"} :monthly (assoc (frame->cron frame day-of-week) :hours hour)))) | |
+----------------------------------------------------------------------------------------------------------------+ | CRON STRING -> SCHEDULE MAP | +----------------------------------------------------------------------------------------------------------------+ | |
(defn- cron->day-of-week [day-of-week] (when-let [[_ day-of-week] (re-matches #"(^\d).*$" day-of-week)] (case day-of-week "1" "sun" "2" "mon" "3" "tue" "4" "wed" "5" "thu" "6" "fri" "7" "sat"))) | |
(defn- cron-day-of-week+day-of-month->frame [day-of-week day-of-month] (cond (re-matches #"^\d#1$" day-of-week) "first" (re-matches #"^\dL$" day-of-week) "last" (= day-of-month "1") "first" (= day-of-month "15") "mid" (= day-of-month "L") "last" :else nil)) | |
(defn- cron->digit [digit] (when (and digit (not= digit "*")) (Integer/parseInt digit))) | |
(defn- cron->schedule-type [hours day-of-month day-of-week] (cond (and day-of-month (not= day-of-month "*") (or (= day-of-week "?") (re-matches #"^\d#1$" day-of-week) (re-matches #"^\dL$" day-of-week))) "monthly" (and day-of-week (not= day-of-week "?")) "weekly" (and hours (not= hours "*")) "daily" :else "hourly")) | |
(mu/defn cron-string->schedule-map :- ScheduleMap "Convert a normal `cron-string` into the expanded ScheduleMap format used by the frontend." [cron-string :- CronScheduleString] (let [[_ mins hours day-of-month _ day-of-week _] (str/split cron-string #"\s+")] {:schedule_minute (cron->digit mins) :schedule_day (cron->day-of-week day-of-week) :schedule_frame (cron-day-of-week+day-of-month->frame day-of-week day-of-month) :schedule_hour (cron->digit hours) :schedule_type (cron->schedule-type hours day-of-month day-of-week)})) | |
(mu/defn describe-cron-string :- ms/NonBlankString "Return a human-readable description of a cron expression, localized for the current User." [^String cron-string :- CronScheduleString] (CronExpressionDescriptor/getDescription cron-string (i18n/user-locale))) | |