1 package com.atlassian.scheduler.core.util;
2
3 import com.atlassian.scheduler.cron.CronSyntaxException;
4 import com.atlassian.scheduler.cron.ErrorCode;
5 import com.google.common.collect.ImmutableMap;
6 import com.google.common.collect.ImmutableSet;
7
8 import java.text.ParseException;
9 import java.util.Map;
10 import java.util.Set;
11 import java.util.regex.Matcher;
12 import java.util.regex.Pattern;
13
14 import static com.atlassian.scheduler.cron.ErrorCode.COMMA_WITH_LAST_DOM;
15 import static com.atlassian.scheduler.cron.ErrorCode.COMMA_WITH_LAST_DOW;
16 import static com.atlassian.scheduler.cron.ErrorCode.COMMA_WITH_NTH_DOW;
17 import static com.atlassian.scheduler.cron.ErrorCode.ILLEGAL_CHARACTER;
18 import static com.atlassian.scheduler.cron.ErrorCode.ILLEGAL_CHARACTER_AFTER_HASH;
19 import static com.atlassian.scheduler.cron.ErrorCode.ILLEGAL_CHARACTER_AFTER_INTERVAL;
20 import static com.atlassian.scheduler.cron.ErrorCode.ILLEGAL_CHARACTER_AFTER_QM;
21 import static com.atlassian.scheduler.cron.ErrorCode.INTERNAL_PARSER_FAILURE;
22 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_NAME;
23 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_NAME_DAY_OF_WEEK;
24 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_NAME_FIELD;
25 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_NAME_MONTH;
26 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_NAME_RANGE;
27 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_NUMBER_DAY_OF_MONTH;
28 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_NUMBER_DAY_OF_MONTH_OFFSET;
29 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_NUMBER_DAY_OF_WEEK;
30 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_NUMBER_HOUR;
31 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_NUMBER_MONTH;
32 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_NUMBER_SEC_OR_MIN;
33 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_NUMBER_YEAR_RANGE;
34 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_STEP;
35 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_STEP_DAY_OF_MONTH;
36 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_STEP_DAY_OF_WEEK;
37 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_STEP_HOUR;
38 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_STEP_MONTH;
39 import static com.atlassian.scheduler.cron.ErrorCode.INVALID_STEP_SECOND_OR_MINUTE;
40 import static com.atlassian.scheduler.cron.ErrorCode.QM_CANNOT_USE_FOR_BOTH_DAYS;
41 import static com.atlassian.scheduler.cron.ErrorCode.QM_CANNOT_USE_HERE;
42 import static com.atlassian.scheduler.cron.ErrorCode.QM_MUST_USE_FOR_ONE_OF_DAYS;
43 import static com.atlassian.scheduler.cron.ErrorCode.UNEXPECTED_TOKEN_FLAG_L;
44 import static com.atlassian.scheduler.cron.ErrorCode.UNEXPECTED_TOKEN_FLAG_W;
45 import static com.atlassian.scheduler.cron.ErrorCode.UNEXPECTED_TOKEN_HASH;
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61 public class QuartzParseExceptionMapper {
62
63 static final ExceptionMapper INVALID_NAME_MAPPER = new InvalidNameMapper();
64 static final ExceptionMapper INVALID_NAME_RANGE_MAPPER = ignoreValue(INVALID_NAME_RANGE);
65 static final ExceptionMapper GENERAL_PARSE_FAILURE_MAPPER = new GeneralParseFailureMapper();
66 static final ExceptionMapper UNEXPECTED_FLAG_L_MAPPER = ignoreValue(UNEXPECTED_TOKEN_FLAG_L);
67 static final ExceptionMapper UNEXPECTED_FLAG_W_MAPPER = ignoreValue(UNEXPECTED_TOKEN_FLAG_W);
68
69
70 private static final Map<String, ErrorCode> SIMPLE_MAPPERS = ImmutableMap.<String, ErrorCode>builder()
71 .put("Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.",
72 QM_MUST_USE_FOR_ONE_OF_DAYS)
73 .put("Support for specifying 'L' and 'LW' with other days of the month is not implemented",
74 COMMA_WITH_LAST_DOM)
75 .put("Support for specifying 'L' with other days of the week is not implemented",
76 COMMA_WITH_LAST_DOW)
77 .put("'?' can only be specfied for Day-of-Month or Day-of-Week.",
78 QM_CANNOT_USE_HERE)
79 .put("'?' can only be specfied for Day-of-Month -OR- Day-of-Week.",
80 QM_CANNOT_USE_FOR_BOTH_DAYS)
81 .put("Support for specifying multiple \"nth\" days is not imlemented.",
82 COMMA_WITH_NTH_DOW)
83 .put("Minute and Second values must be between 0 and 59", INVALID_NUMBER_SEC_OR_MIN)
84 .put("Hour values must be between 0 and 23", INVALID_NUMBER_HOUR)
85 .put("Day of month values must be between 1 and 31", INVALID_NUMBER_DAY_OF_MONTH)
86 .put("Month values must be between 1 and 12", INVALID_NUMBER_MONTH)
87 .put("Day-of-Week values must be between 1 and 7", INVALID_NUMBER_DAY_OF_WEEK)
88 .put("Offset from last day must be <= 30", INVALID_NUMBER_DAY_OF_MONTH_OFFSET)
89 .put("'/' must be followed by an integer.", INVALID_STEP)
90 .put("A numeric value between 1 and 5 must follow the '#' option",
91 ILLEGAL_CHARACTER_AFTER_HASH)
92 .put("The 'W' option does not make sense with values larger than 31 (max number of days in a month)",
93 INVALID_NUMBER_DAY_OF_MONTH)
94 .put("Illegal cron expression format (java.lang.IllegalArgumentException: Start year must be less than stop year)",
95 INVALID_NUMBER_YEAR_RANGE)
96 .build();
97
98
99
100 private static final Map<String, ExceptionMapper> PREFIX_MAPPERS = ImmutableMap.<String, ExceptionMapper>builder()
101 .put("Unexpected end of expression.", new UnexpectedEndOfExpressionMapper())
102 .put("Invalid Day-of-Week value: '", new RemovePrefixAndSuffix("'", new InvalidDayOfWeekNameMapper()))
103 .put("Invalid Month value: '", new RemovePrefixAndSuffix("'", new InvalidMonthNameMapper()))
104 .put("Illegal character after '?': ",
105 new SingleCharAfterPrefix(error(ILLEGAL_CHARACTER_AFTER_QM)))
106 .put("Illegal characters for this position: '",
107 new RemovePrefixAndSuffix("'", new IllegalCharactersMapper()))
108 .put("Increment > 60 : ", new RemovePrefix(error(INVALID_STEP_SECOND_OR_MINUTE)))
109 .put("Increment > 31 : ", new RemovePrefix(error(INVALID_STEP_DAY_OF_MONTH)))
110 .put("Increment > 24 : ", new RemovePrefix(error(INVALID_STEP_HOUR)))
111 .put("Increment > 7 : ", new RemovePrefix(error(INVALID_STEP_DAY_OF_WEEK)))
112 .put("Increment > 12 : ", new RemovePrefix(error(INVALID_STEP_MONTH)))
113 .put("Unexpected character: ", new SingleCharAfterPrefix(error(ILLEGAL_CHARACTER)))
114 .put("Unexpected character '",
115 new RemovePrefixAndSuffix("' after '/'", error(ILLEGAL_CHARACTER_AFTER_INTERVAL)))
116 .put("'L' option is not valid here. (pos=", new RemovePrefixAndSuffix(")", UNEXPECTED_FLAG_L_MAPPER))
117 .put("'W' option is not valid here. (pos=", new RemovePrefixAndSuffix(")", UNEXPECTED_FLAG_W_MAPPER))
118 .put("'#' option is not valid here. (pos=",
119 new RemovePrefixAndSuffix(")", ignoreValue(UNEXPECTED_TOKEN_HASH)))
120 .put("Illegal cron expression format (java.lang.NumberFormatException: For input string: \"",
121 new RemovePrefixAndSuffix("\")", new NumberFormatExceptionMapper()))
122 .put("Illegal cron expression format (java.lang.StringIndexOutOfBoundsException: String index out of range: ",
123 new RemovePrefixAndSuffix(")", new StringIndexOutOfBoundsMapper()))
124 .put("Illegal cron expression format (",
125 new RemovePrefixAndSuffix(")", GENERAL_PARSE_FAILURE_MAPPER))
126 .build();
127
128
129 public static CronSyntaxException mapException(final String cronExpression, final ParseException pe) {
130 final String message = pe.getMessage();
131 if (message == null) {
132 return mapGeneral(cronExpression, pe);
133 }
134
135 final ErrorCode errorCode = SIMPLE_MAPPERS.get(message);
136 if (errorCode != null) {
137 return CronSyntaxException.builder()
138 .cronExpression(cronExpression)
139 .errorCode(errorCode)
140 .cause(pe)
141 .build();
142 }
143
144 return mapExceptionByPrefix(cronExpression, pe);
145 }
146
147 private static CronSyntaxException mapExceptionByPrefix(final String cronExpression, final ParseException pe) {
148 final String message = pe.getMessage();
149 for (Map.Entry<String, ExceptionMapper> entry : PREFIX_MAPPERS.entrySet()) {
150 final String prefix = entry.getKey();
151 if (message.startsWith(prefix)) {
152 return entry.getValue().map(cronExpression, pe, prefix);
153 }
154 }
155
156 return mapGeneral(cronExpression, pe);
157 }
158
159 private static CronSyntaxException mapGeneral(final String cronExpression, final ParseException pe) {
160 Throwable cause = pe.getCause();
161 if (cause == null) {
162 cause = pe;
163 }
164 return CronSyntaxException.builder()
165 .cronExpression(cronExpression)
166 .errorCode(INTERNAL_PARSER_FAILURE)
167 .cause(cause)
168 .value(pe.getMessage())
169 .build();
170 }
171
172 static boolean startsWithNumber(final String s) {
173 if (s.isEmpty()) {
174 return false;
175 }
176 final char c = s.charAt(0);
177 return c >= '0' && c <= '9';
178 }
179
180
181
182
183 static interface ExceptionMapper {
184 CronSyntaxException map(String cronExpression, ParseException pe, String value);
185 }
186
187 static class RemovePrefix implements ExceptionMapper {
188 private final ExceptionMapper delegate;
189
190 RemovePrefix(final ExceptionMapper delegate) {
191 this.delegate = delegate;
192 }
193
194 @Override
195 public CronSyntaxException map(final String cronExpression, final ParseException pe, final String prefix) {
196 return delegate.map(cronExpression, pe, pe.getMessage().substring(prefix.length()));
197 }
198 }
199
200 static class SingleCharAfterPrefix implements ExceptionMapper {
201 private final ExceptionMapper delegate;
202
203 SingleCharAfterPrefix(ExceptionMapper delegate) {
204 this.delegate = delegate;
205 }
206
207 @Override
208 public CronSyntaxException map(final String cronExpression, final ParseException pe, final String prefix) {
209 final char c = pe.getMessage().charAt(prefix.length());
210 return delegate.map(cronExpression, pe, String.valueOf(c));
211 }
212 }
213
214 static class RemovePrefixAndSuffix implements ExceptionMapper {
215 private final String suffix;
216 private final ExceptionMapper delegate;
217
218 RemovePrefixAndSuffix(final String suffix, final ExceptionMapper delegate) {
219 this.suffix = suffix;
220 this.delegate = delegate;
221 }
222
223 @Override
224 public CronSyntaxException map(final String cronExpression, final ParseException pe, final String prefix) {
225 final String value = pe.getMessage();
226
227 final int pos = value.lastIndexOf(suffix);
228 if (pos > prefix.length()) {
229 return delegate.map(cronExpression, pe, value.substring(prefix.length(), pos));
230 }
231
232
233 return delegate.map(cronExpression, pe, value.substring(prefix.length()));
234 }
235 }
236
237 static class ErrorCodeMapper implements ExceptionMapper {
238 private final ErrorCode errorCode;
239
240 ErrorCodeMapper(final ErrorCode errorCode) {
241 this.errorCode = errorCode;
242 }
243
244 @Override
245 public CronSyntaxException map(String cronExpression, ParseException pe, String value) {
246 return CronSyntaxException.builder()
247 .cronExpression(cronExpression)
248 .errorCode(errorCode)
249 .cause(pe)
250 .value(value)
251 .build();
252 }
253 }
254
255 static class IgnoreValue extends ErrorCodeMapper {
256 IgnoreValue(ErrorCode errorCode) {
257 super(errorCode);
258 }
259
260 @Override
261 public CronSyntaxException map(String cronExpression, ParseException pe, String value) {
262 return super.map(cronExpression, pe, null);
263 }
264 }
265
266
267 static class UnexpectedEndOfExpressionMapper implements ExceptionMapper {
268 @Override
269 public CronSyntaxException map(String cronExpression, ParseException pe, String value) {
270 return CronSyntaxException.builder()
271 .cronExpression(cronExpression)
272 .errorCode(ErrorCode.UNEXPECTED_END_OF_EXPRESSION)
273 .errorOffset(cronExpression.length())
274 .cause(pe)
275 .build();
276 }
277 }
278
279
280
281
282
283
284
285 static class IllegalCharactersMapper implements ExceptionMapper {
286 private static final ExceptionMapper WRONG_FIELD = error(INVALID_NAME_FIELD);
287
288 @Override
289 public CronSyntaxException map(String cronExpression, ParseException pe, String value) {
290 final int hyphen = value.indexOf('-');
291 final String s = (hyphen != -1) ? value.substring(0, hyphen) : value;
292
293 if ("L".equals(s)) {
294 return UNEXPECTED_FLAG_L_MAPPER.map(cronExpression, pe, null);
295 }
296
297 if ("W".equals(s)) {
298 return UNEXPECTED_FLAG_W_MAPPER.map(cronExpression, pe, null);
299 }
300
301 return WRONG_FIELD.map(cronExpression, pe, value);
302 }
303 }
304
305
306
307
308
309
310
311
312
313
314
315
316
317 static class InvalidNameMapper implements ExceptionMapper {
318 private static final Pattern REGEX_FIND_NAMES = Pattern.compile("[A-Z]+");
319 private static final Pattern REGEX_FIND_BAD_FLAG_L = Pattern.compile("[^A-Z]L-( |\t|$)");
320 private static final Set<String> L_AND_W_FLAGS = ImmutableSet.of("L", "W", "LW");
321
322 @Override
323 public CronSyntaxException map(String cronExpression, ParseException pe, String ignored) {
324 Matcher matcher = REGEX_FIND_NAMES.matcher(cronExpression);
325 while (matcher.find()) {
326 final String name = matcher.group(0);
327 if (name.length() < 3 && !L_AND_W_FLAGS.contains(name)) {
328 return CronSyntaxException.builder()
329 .cronExpression(cronExpression)
330 .errorCode(INVALID_NAME)
331 .errorOffset(matcher.start())
332 .value(name)
333 .cause(pe)
334 .build();
335 }
336 }
337
338 matcher = REGEX_FIND_BAD_FLAG_L.matcher(cronExpression);
339 if (matcher.find()) {
340 return CronSyntaxException.builder()
341 .cronExpression(cronExpression)
342 .errorCode(UNEXPECTED_TOKEN_FLAG_L)
343 .errorOffset(matcher.start() + 1)
344 .cause(pe)
345 .build();
346 }
347
348
349 return mapGeneral(cronExpression, pe);
350 }
351 }
352
353 static class InvalidDayOfWeekNameMapper implements ExceptionMapper {
354 private static final ExceptionMapper BAD_DAY_OF_WEEK = error(INVALID_NAME_DAY_OF_WEEK);
355
356 @Override
357 public CronSyntaxException map(String cronExpression, ParseException pe, String value) {
358
359 final ExceptionMapper mapper = startsWithNumber(value) ? INVALID_NAME_RANGE_MAPPER : BAD_DAY_OF_WEEK;
360 return mapper.map(cronExpression, pe, value);
361 }
362 }
363
364 static class InvalidMonthNameMapper implements ExceptionMapper {
365 private static final ExceptionMapper BAD_MONTH = error(INVALID_NAME_MONTH);
366
367 @Override
368 public CronSyntaxException map(String cronExpression, ParseException pe, String value) {
369
370 final ExceptionMapper mapper = startsWithNumber(value) ? INVALID_NAME_RANGE_MAPPER : BAD_MONTH;
371 return mapper.map(cronExpression, pe, value);
372 }
373 }
374
375
376
377
378 static class NumberFormatExceptionMapper implements ExceptionMapper {
379 private static final Pattern REGEX_FIND_BAD_RANGE = Pattern.compile("[0-9]+-([A-Za-z]+)");
380 private static final Pattern REGEX_FIND_BAD_STEP = Pattern.compile("/[^0-9]");
381
382 @Override
383 public CronSyntaxException map(String cronExpression, ParseException pe, String ignored) {
384 final Matcher range = REGEX_FIND_BAD_RANGE.matcher(cronExpression);
385 final Matcher step = REGEX_FIND_BAD_STEP.matcher(cronExpression);
386
387 if (range.find()) {
388 if (step.find() && step.start() < range.start()) {
389 return mapStep(cronExpression, pe, step);
390 }
391 return mapRange(cronExpression, pe, range);
392 }
393
394 if (step.find()) {
395 return mapStep(cronExpression, pe, step);
396 }
397
398
399 return mapGeneral(cronExpression, pe);
400 }
401
402 private static CronSyntaxException mapRange(String cronExpression, ParseException pe, Matcher range) {
403 return CronSyntaxException.builder()
404 .cronExpression(cronExpression)
405 .errorCode(INVALID_NAME_RANGE)
406 .errorOffset(range.start(1))
407 .cause(pe)
408 .build();
409 }
410
411 private static CronSyntaxException mapStep(String cronExpression, ParseException pe, Matcher step) {
412 return CronSyntaxException.builder()
413 .cronExpression(cronExpression)
414 .errorCode(INVALID_STEP)
415 .errorOffset(step.start() + 1)
416 .cause(pe)
417 .build();
418 }
419 }
420
421
422
423
424 static class StringIndexOutOfBoundsMapper implements ExceptionMapper {
425 @Override
426 public CronSyntaxException map(String cronExpression, ParseException pe, String value) {
427 final int len = toInt(value);
428 switch (len) {
429 case 1:
430 case 2:
431 case 3:
432 return INVALID_NAME_MAPPER.map(cronExpression, pe, null);
433
434 case 5:
435 case 6:
436 case 7:
437 return INVALID_NAME_RANGE_MAPPER.map(cronExpression, pe, null);
438 }
439
440
441 return mapGeneral(cronExpression, pe);
442 }
443 }
444
445 static class GeneralParseFailureMapper implements ExceptionMapper {
446 @Override
447 public CronSyntaxException map(String cronExpression, ParseException pe, String value) {
448 return mapGeneral(cronExpression, pe);
449 }
450 }
451
452 static int toInt(String s) {
453 try {
454 if (s != null) {
455 return Integer.parseInt(s);
456 }
457 } catch (NumberFormatException nfe) {
458
459 }
460 return -1;
461 }
462
463 private static ErrorCodeMapper error(ErrorCode errorCode) {
464 return new ErrorCodeMapper(errorCode);
465 }
466
467 private static IgnoreValue ignoreValue(ErrorCode errorCode) {
468 return new IgnoreValue(errorCode);
469 }
470 }