1 package com.atlassian.core.cron.parser;
2
3 import com.atlassian.core.util.collection.EasyList;
4 import org.apache.log4j.Logger;
5
6 import java.util.List;
7
8 /**
9 * Represents the hours part of a cron string. This class is responsible for parsing and validating the hours entry.
10 * The {@link #isValid()} method refers only to what is supported by the cron editor for the hours entry.
11 * <p/>
12 * Valid hours means a numerical hour (or range of the form x-y) with an optional trailing "/1", "/2" or "/3" to
13 * indicate the repeat increment in hours. This expects that numeric hours to be in 24 hour time format.
14 * <p/>
15 * The hours and meridian attributes depend on whether this is a range entry or a "run once" entry. For ranges, use
16 * the "from" and "to" methods.
17 */
18 public class CronHoursEntry
19 {
20 private static final Logger log = Logger.getLogger(CronHoursEntry.class);
21
22 /**
23 * cronEntry should only contain legal characters are '/', '*', '-', and digit
24 */
25 private static final String REGEX_VALID = "[\\d*/-]+";
26
27 /**
28 * Flag to indicate no increment part is present.
29 */
30 private static final int NO_INCREMENT_PART = -1;
31
32 /**
33 * The set of hour increments that we accept. This is derived from the minimum requirements of the cron spec editor.
34 */
35 private static final List ACCEPTED_HOUR_INCREMENTS = EasyList.build(new Integer(NO_INCREMENT_PART), new Integer(1), new Integer(2), new Integer(3));
36
37 static final String INCREMENT_DELIMITER = "/";
38 static final String RANGE_DELIMITER = "-";
39
40 private static final MeridianHour NULL_MERIDIAN_HOUR = new MeridianHour(NO_INCREMENT_PART, null);
41
42 private MeridianHour fromMeridianHour = NULL_MERIDIAN_HOUR;
43 private MeridianHour toMeridianHour = NULL_MERIDIAN_HOUR;
44
45 private MeridianHour runOnceMeridianHour = NULL_MERIDIAN_HOUR;
46
47 private int increment = NO_INCREMENT_PART;
48 private boolean valid = true;
49
50 /**
51 * Parses the given value and establishes state based on this.
52 * @param cronEntry the hours field of a cron string.
53 */
54 public CronHoursEntry(String cronEntry)
55 {
56 if (cronEntry == null)
57 {
58 throw new IllegalArgumentException("Can not create a cron entry from a null value.");
59 }
60 parseEntry(cronEntry);
61 }
62
63 /**
64 * Returns true only if the hours entry is valid with respect to the editor.
65 * @return true only if the editor can handle this hour part.
66 */
67 public boolean isValid()
68 {
69 return valid && ACCEPTED_HOUR_INCREMENTS.contains(new Integer(increment));
70 }
71
72 /**
73 * Returns the lower bound of the hour range if this entry has a range.
74 *
75 * This end of the range is inclusive - e.g. if HoursFrom is 3PM and HoursTo is 5PM, the duration of the range
76 * is 2 hours.
77 *
78 * @return the lower bound or -1 if this is not a range hour entry.
79 */
80 public int getFrom()
81 {
82 return fromMeridianHour.getHour();
83 }
84
85 /**
86 * Returns the upper bound of the hour range if this entry has a range.
87 *
88 * This end of the range is exclusive - e.g. if HoursFrom is 3PM and HoursTo is 5PM, the duration of the range
89 * is 2 hours.
90 *
91 * @return the upper bound or -1 if this is not a range hour entry.
92 */
93 public int getTo()
94 {
95 return toMeridianHour.getHour();
96 }
97
98 /**
99 * Returns the meridian indicator @{link #AM} or @{link #AM} for the lower bound of a range entry.
100 *
101 * @return the meridian belonging to the lower bound hour or null if this is not a range entry.
102 */
103 public String getFromMeridian()
104 {
105 return fromMeridianHour.getMeridian();
106 }
107
108 /**
109 * Returns the meridian indicator @{link #AM} or @{link #AM} for the upper bound of a range entry.
110 *
111 * @return the meridian belonging to the upper bound hour or null if this is not a range entry.
112 */
113 public String getToMeridian()
114 {
115 return toMeridianHour.getMeridian();
116 }
117
118 /**
119 * Returns the single hour value for this entry if it is a run once entry.
120 *
121 * @return the hour value or -1 if this is not a run once hour entry.
122 */
123 public int getRunOnce()
124 {
125 return runOnceMeridianHour.getHour();
126 }
127
128 /**
129 * Returns the meridian indicator @{link #AM} or @{link #AM} for the entry if it is a run once entry.
130 *
131 * @return the meridian belonging single hour value or null if this is not a run once entry.
132 */
133 public String getRunOnceMeridian()
134 {
135 return runOnceMeridianHour.getMeridian();
136 }
137
138 /**
139 * Returns the increment or step size in hours or -1 if this entry has no increment.
140 *
141 * @return the period of repetition in hours.
142 */
143 public int getIncrement()
144 {
145 return increment;
146 }
147
148 /**
149 * Indicates if this entry has an increment.
150 *
151 * @return true only if the entry has an increment part specified.
152 */
153 public boolean hasIncrement()
154 {
155 return increment != NO_INCREMENT_PART;
156 }
157
158 public boolean isRunOnce()
159 {
160 return !NULL_MERIDIAN_HOUR.equals(runOnceMeridianHour);
161 }
162
163 /**
164 * Sets the state of this entry based on the given cron entry including the validity.
165 *
166 * @param cronEntry the cron entry part for hours.
167 */
168 private void parseEntry(String cronEntry)
169 {
170 if (!cronEntry.matches(REGEX_VALID))
171 {
172 valid = false;
173 }
174 else
175 {
176 // a '*' denotes "every hour", so it has an increment of 1, and starts at 12 AM and finishes at 12 AM
177 if ("*".equals(cronEntry))
178 {
179 this.increment = 1;
180 this.fromMeridianHour = parseMeridianHour("0");
181 this.toMeridianHour = this.fromMeridianHour;
182 return;
183 }
184
185 int slashIndex = cronEntry.indexOf(INCREMENT_DELIMITER);
186 if (slashIndex >= 0)
187 {
188 String incrementStr = cronEntry.substring(slashIndex + 1, cronEntry.length());
189 try
190 {
191 this.increment = Integer.parseInt(incrementStr);
192 }
193 catch (NumberFormatException nfe)
194 {
195 log.debug("The increment portion of the hour cron entry must be an integer.");
196 valid = false;
197 }
198 //Chop this off, we don't need it anymore
199 cronEntry = cronEntry.substring(0, slashIndex);
200 }
201
202 int dashIndex = cronEntry.indexOf(RANGE_DELIMITER);
203 if (dashIndex >= 0)
204 {
205 String fromStr = cronEntry.substring(0, dashIndex);
206 this.fromMeridianHour = parseMeridianHour(fromStr);
207
208 String toStr = cronEntry.substring(dashIndex + 1, cronEntry.length());
209 // JRA-13503: since toMeridianHour is exclusive, but cron expressions are inclusive,
210 // we need to increment the "to" hour in the cron expression by 1 to get the correct representation
211 this.toMeridianHour = parseMeridianHour(incrementHourByOne(toStr));
212 }
213 // if we have specified a "24-range" e.g. 4am - 4am, the cron entry will not contain the range delimiter,
214 // but it will have an increment.
215 else if (hasIncrement())
216 {
217 this.fromMeridianHour = parseMeridianHour(cronEntry);
218 this.toMeridianHour = parseMeridianHour(cronEntry);
219 }
220 else
221 {
222 runOnceMeridianHour = parseMeridianHour(cronEntry);
223 }
224 }
225 }
226
227 private MeridianHour parseMeridianHour(String twentyFourHour)
228 {
229 MeridianHour meridianHour;
230 // '*' means any hour, so default to 12AM
231 if ("*".equals(twentyFourHour))
232 {
233 twentyFourHour = "0";
234 }
235 meridianHour = MeridianHour.parseMeridianHour(twentyFourHour);
236 if (meridianHour == null)
237 {
238 valid = false;
239 meridianHour = NULL_MERIDIAN_HOUR;
240 }
241 return meridianHour;
242 }
243
244 private String incrementHourByOne(String hour)
245 {
246 // don't use modulo operator here, because if the hour has
247 // exceeded the valid range, using modulo will change it
248 int h = Integer.parseInt(hour);
249 h = (h == 23) ? 0 : h + 1;
250 return "" + h;
251 }
252
253 }