1 package com.atlassian.scheduler.core.util;
2
3 import javax.annotation.Nullable;
4 import java.util.Random;
5 import java.util.regex.Pattern;
6
7 /**
8 * Utility class to prevent the use of cron expressions that run more frequently than once per minute.
9 * It is assumed that the cron expression has already been validated or that it is acceptable for the
10 * validation to return the "wrong" message when the expression is validated later on.
11 * <p>
12 * Without this, people could supply cron expressions that run every five seconds or something
13 * equally inappropriate.
14 * </p>
15 *
16 * @since v1.0
17 */
18 public class CronExpressionQuantizer {
19 private static final Pattern REGEX = Pattern.compile("^[0-5]?[0-9] ");
20 private static final Random RANDOM = new Random();
21
22 /**
23 * Quantize the seconds field using {@link Randomize#AS_NEEDED}.
24 *
25 * @param cronExpression the cron expression to (possibly) quantize.
26 * @return the cron expression with the seconds field forced to a random value between {@code 0} and
27 * {@code 59} (inclusive) if it is not already in that format
28 */
29 public static String quantizeSecondsField(String cronExpression) {
30 return quantizeSecondsField(cronExpression, Randomize.AS_NEEDED);
31 }
32
33 /**
34 * Quantize the seconds field, randomizing it as requested.
35 *
36 * @param cronExpression the cron expression to (possibly) quantize.
37 * @param randomize controls the randomization of the seconds fields ({@code null} is assumed to mean
38 * {@link Randomize#AS_NEEDED}.
39 * @return the cron expression with the seconds field forced to a value between {@code 0} and
40 * {@code 59} (inclusive) if it is not already in that format or as instructed to by
41 * the {@code randomize} option.
42 */
43 public static String quantizeSecondsField(@Nullable String cronExpression, @Nullable Randomize randomize) {
44 if (!shouldEdit(cronExpression, randomize)) {
45 return cronExpression;
46 }
47
48 final int pos = cronExpression.indexOf(' ');
49 if (pos <= 0) {
50 return cronExpression;
51 }
52
53 final StringBuilder sb = new StringBuilder(cronExpression.length());
54 sb.append((randomize != Randomize.NEVER) ? RANDOM.nextInt(60) : 0);
55 return sb.append(cronExpression, pos, cronExpression.length()).toString();
56 }
57
58 @SuppressWarnings("SimplifiableIfStatement")
59 private static boolean shouldEdit(@Nullable String cronExpression, @Nullable Randomize randomize) {
60 if (cronExpression == null) {
61 return false;
62 }
63 if (randomize == Randomize.ALWAYS) {
64 return true;
65 }
66 return !REGEX.matcher(cronExpression).find();
67 }
68
69 /**
70 * Controls whether or not randomization is performed when a cron expression is quantized.
71 */
72 public static enum Randomize {
73 /**
74 * Never randomize the seconds field. If the supplied value does not meet the requirement (a simple
75 * integer from {@code 0} to {@code 59}), then force it to {@code 0}.
76 */
77 NEVER,
78
79 /**
80 * Randomize the seconds field if quantization is needed; otherwise, leave it alone. If the supplied
81 * value does not meet the requirement (a simple integer from {@code 0} to {@code 59}), then force it
82 * to a randomized value; otherwise, leave it alone.
83 */
84 AS_NEEDED,
85
86 /**
87 * Replace the seconds fields with a randomized value in all cases. The replacement is performed
88 * regardless of whether or not the value was already in the permitted format.
89 */
90 ALWAYS
91 }
92 }