PageRenderTime 46ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/jira-project/jira-components/jira-core/src/main/java/com/atlassian/jira/upgrade/tasks/util/CronExpressionFixer.java

https://bitbucket.org/ahmed_bilal_360factors/jira7-core
Java | 275 lines | 162 code | 43 blank | 70 comment | 30 complexity | 5c8ffca7edbbea0d07fa1f63a88b682e MD5 | raw file
Possible License(s): Apache-2.0
  1. package com.atlassian.jira.upgrade.tasks.util;
  2. import com.atlassian.scheduler.caesium.cron.parser.CronExpressionParser;
  3. import com.atlassian.scheduler.cron.CronSyntaxException;
  4. import javax.annotation.Nonnull;
  5. import java.util.Optional;
  6. import static com.atlassian.jira.util.dbc.Assertions.notNull;
  7. /**
  8. * Utility for repairing a cron expression that is failing to parse under Caesium but was
  9. * apparently previously allowed by Quartz.
  10. * <p>
  11. * There are certain error cases that Quartz ignored but that Caesium enforces. This tool
  12. * attempts to identify these problems and repair the cron expression. The type of repair
  13. * done is intended to preserve the spirit of the original expression as closely as possible.
  14. * The following specific cases are handled:
  15. * </p>
  16. * <table summary="Repair strategies">
  17. * <thead>
  18. * <tr><th>Problem</th><th>Example</th><th>Repaired</th><th>Comment</th></tr>
  19. * </thead>
  20. * <tbody>
  21. * <tr>
  22. * <th>Invalid step interval</th>
  23. * <td>0 0/60 * * * ?</td>
  24. * <td>0 0 * * * ?</td>
  25. * <td>Quartz ignores the invalid interval, so throwing it away has the same result</td>
  26. * </tr>
  27. * <tr>
  28. * <th>Multiple values with W</th>
  29. * <td>0 0 0 1W,15W * ?</td>
  30. * <td>0 0 0 1,15 * ?</td>
  31. * <td>Quartz either ignores all of the W flags or only processes the first value,
  32. * depending on which came first. Throwing away the W flags is the easiest way
  33. * to come as close as we can to what they asked for.</td>
  34. * </tr>
  35. * <tr>
  36. * <th>Illegal characters</th>
  37. * <td>0 0 0 ? * *?</td>
  38. * <td>0 0 0 ? * *</td>
  39. * <td>Quartz ignores the stray {@code ?} at the end. This repair code will try
  40. * just deleting the illegal character.</td></tr>
  41. * </tbody>
  42. * </table>
  43. * <p>
  44. * If the cron expression parser returns an error that is not covered by any of these cases, or if
  45. * after {@code 5} attempts to fix the expression it still has problems, we give up.
  46. * </p>
  47. *
  48. * @since v7.0.0
  49. */
  50. public class CronExpressionFixer {
  51. private static final int MAX_ATTEMPTS = 5;
  52. private final String originalCronExpression;
  53. private final CronSyntaxException originalException;
  54. private String cronExpression;
  55. private CronSyntaxException exception;
  56. /**
  57. * Attempts to repair a possibly broken cron expression.
  58. *
  59. * @param cronExpression the cron expression to repair
  60. * @return the result of the attempted repair
  61. */
  62. @Nonnull
  63. public static Result repairCronExpression(final String cronExpression) {
  64. notNull("cronExpression", cronExpression);
  65. final CronSyntaxException exception = findNextSyntaxError(cronExpression);
  66. if (exception == null) {
  67. return Result.ALREADY_VALID;
  68. }
  69. return new CronExpressionFixer(cronExpression, exception).repair();
  70. }
  71. private CronExpressionFixer(@Nonnull final String originalCronExpression, @Nonnull final CronSyntaxException originalException) {
  72. this.originalCronExpression = originalCronExpression;
  73. this.originalException = originalException;
  74. }
  75. private Result repair() {
  76. cronExpression = originalCronExpression;
  77. exception = originalException;
  78. for (int attempt = 1; attempt <= MAX_ATTEMPTS && tryRepair(); ++attempt) {
  79. exception = findNextSyntaxError(cronExpression);
  80. if (exception == null) {
  81. return Result.success(cronExpression);
  82. }
  83. }
  84. return Result.failure(originalException);
  85. }
  86. private boolean tryRepair() {
  87. switch (exception.getErrorCode()) {
  88. case INVALID_STEP:
  89. case INVALID_STEP_SECOND_OR_MINUTE:
  90. case INVALID_STEP_HOUR:
  91. case INVALID_STEP_DAY_OF_MONTH:
  92. case INVALID_STEP_MONTH:
  93. case INVALID_STEP_DAY_OF_WEEK:
  94. return discardInvalidStep(exception.getErrorOffset() - 1);
  95. case COMMA_WITH_WEEKDAY_DOM:
  96. case UNEXPECTED_TOKEN_FLAG_W:
  97. return discardWeekdayFlags(exception.getErrorOffset());
  98. case ILLEGAL_CHARACTER:
  99. case ILLEGAL_CHARACTER_AFTER_HASH:
  100. case ILLEGAL_CHARACTER_AFTER_INTERVAL:
  101. case ILLEGAL_CHARACTER_AFTER_QM:
  102. return discardIllegalCharacter(exception.getErrorOffset());
  103. default:
  104. return false;
  105. }
  106. }
  107. private boolean discardInvalidStep(final int offset) {
  108. if (isOutOfBounds(offset) || cronExpression.charAt(offset) != '/') {
  109. return false;
  110. }
  111. deleteSubstring(offset, searchForwardForEndOfField(offset));
  112. return true;
  113. }
  114. private boolean discardWeekdayFlags(final int offset) {
  115. if (isOutOfBounds(offset)) {
  116. return false;
  117. }
  118. final int start = searchBackwardForStartOfField(offset);
  119. int stop = searchForwardForEndOfField(offset);
  120. final StringBuilder sb = new StringBuilder(cronExpression);
  121. for (int i = start; i < stop; ++i) {
  122. final char c = sb.charAt(i);
  123. if (c == 'w' || c == 'W') {
  124. sb.deleteCharAt(i);
  125. --i;
  126. --stop;
  127. }
  128. }
  129. cronExpression = sb.toString();
  130. return true;
  131. }
  132. private boolean discardIllegalCharacter(final int offset) {
  133. if (isOutOfBounds(offset)) {
  134. return false;
  135. }
  136. deleteSubstring(offset, offset + 1);
  137. return true;
  138. }
  139. private void deleteSubstring(final int offset, final int stop) {
  140. final StringBuilder sb = new StringBuilder(cronExpression.length());
  141. sb.append(cronExpression, 0, offset);
  142. if (stop < cronExpression.length()) {
  143. sb.append(cronExpression, stop, cronExpression.length());
  144. }
  145. cronExpression = sb.toString();
  146. }
  147. private int searchBackwardForStartOfField(final int offset) {
  148. int start = offset - 1;
  149. while (start >= 0 && !isSpace(cronExpression.charAt(start))) {
  150. --start;
  151. }
  152. return start + 1;
  153. }
  154. private int searchForwardForEndOfField(final int offset) {
  155. int stop = offset + 1;
  156. while (stop < cronExpression.length() && !isSpace(cronExpression.charAt(stop))) {
  157. ++stop;
  158. }
  159. return stop;
  160. }
  161. private boolean isOutOfBounds(int offset) {
  162. return offset < 0 || offset >= cronExpression.length();
  163. }
  164. private static boolean isSpace(char c) {
  165. return c == ' ' || c == '\t';
  166. }
  167. private static CronSyntaxException findNextSyntaxError(final String cronExpression) {
  168. try {
  169. CronExpressionParser.parse(cronExpression);
  170. return null;
  171. } catch (CronSyntaxException cse) {
  172. return cse;
  173. }
  174. }
  175. /**
  176. * Represents the result of an attempt to repair a cron expression.
  177. * <ul>
  178. * <li>If the repair is unnecessary, then both {@link #getNewCronExpression()} and
  179. * {@link #getCronSyntaxException()} will be {@link Optional#empty()}.</li>
  180. * <li>If the repair was needed and succeeds, then {@link #getNewCronExpression()} returns the repaired
  181. * cron expression and {@link #getCronSyntaxException()} is {@link Optional#empty()}.</li>
  182. * <li>If the cron expression is broken but cannot be repaired, then {@link #getCronSyntaxException()} returns
  183. * the first syntax error found and {@link #getNewCronExpression()} is {@link Optional#empty()}.</li>
  184. * </ul>
  185. */
  186. public static class Result {
  187. static final Result ALREADY_VALID = new Result(Optional.empty(), Optional.empty());
  188. private final Optional<String> newCronExpression;
  189. private final Optional<CronSyntaxException> exception;
  190. private Result(Optional<String> newCronExpression, Optional<CronSyntaxException> exception) {
  191. this.newCronExpression = newCronExpression;
  192. this.exception = exception;
  193. }
  194. /**
  195. * Returns the first syntax error found for the cron expression (if the repair failed)
  196. *
  197. * @return the first syntax error found for the cron expression (if the repair failed)
  198. */
  199. public Optional<CronSyntaxException> getCronSyntaxException() {
  200. return exception;
  201. }
  202. /**
  203. * Returns the repaired cron expression (if the repair succeeded).
  204. *
  205. * @return the repaired cron expression (if the repair succeeded).
  206. */
  207. public Optional<String> getNewCronExpression() {
  208. return newCronExpression;
  209. }
  210. static Result success(String newCronExpression) {
  211. return new Result(Optional.of(newCronExpression), Optional.empty());
  212. }
  213. static Result failure(CronSyntaxException exception) {
  214. return new Result(Optional.empty(), Optional.of(exception));
  215. }
  216. @Override
  217. public boolean equals(final Object o) {
  218. return this == o || (o instanceof Result && equals((Result) o));
  219. }
  220. private boolean equals(@Nonnull Result other) {
  221. return newCronExpression.equals(other.newCronExpression) && exception.equals(other.exception);
  222. }
  223. @Override
  224. public int hashCode() {
  225. return 31 * newCronExpression.hashCode() + exception.hashCode();
  226. }
  227. @Override
  228. public String toString() {
  229. return "Result[newCronExpression=" + newCronExpression + ", exception=" + exception + ']';
  230. }
  231. }
  232. }