/jira-project/jira-components/jira-core/src/main/java/com/atlassian/jira/upgrade/tasks/util/CronExpressionFixer.java
Java | 275 lines | 162 code | 43 blank | 70 comment | 30 complexity | 5c8ffca7edbbea0d07fa1f63a88b682e MD5 | raw file
Possible License(s): Apache-2.0
- package com.atlassian.jira.upgrade.tasks.util;
- import com.atlassian.scheduler.caesium.cron.parser.CronExpressionParser;
- import com.atlassian.scheduler.cron.CronSyntaxException;
- import javax.annotation.Nonnull;
- import java.util.Optional;
- import static com.atlassian.jira.util.dbc.Assertions.notNull;
- /**
- * Utility for repairing a cron expression that is failing to parse under Caesium but was
- * apparently previously allowed by Quartz.
- * <p>
- * There are certain error cases that Quartz ignored but that Caesium enforces. This tool
- * attempts to identify these problems and repair the cron expression. The type of repair
- * done is intended to preserve the spirit of the original expression as closely as possible.
- * The following specific cases are handled:
- * </p>
- * <table summary="Repair strategies">
- * <thead>
- * <tr><th>Problem</th><th>Example</th><th>Repaired</th><th>Comment</th></tr>
- * </thead>
- * <tbody>
- * <tr>
- * <th>Invalid step interval</th>
- * <td>0 0/60 * * * ?</td>
- * <td>0 0 * * * ?</td>
- * <td>Quartz ignores the invalid interval, so throwing it away has the same result</td>
- * </tr>
- * <tr>
- * <th>Multiple values with W</th>
- * <td>0 0 0 1W,15W * ?</td>
- * <td>0 0 0 1,15 * ?</td>
- * <td>Quartz either ignores all of the W flags or only processes the first value,
- * depending on which came first. Throwing away the W flags is the easiest way
- * to come as close as we can to what they asked for.</td>
- * </tr>
- * <tr>
- * <th>Illegal characters</th>
- * <td>0 0 0 ? * *?</td>
- * <td>0 0 0 ? * *</td>
- * <td>Quartz ignores the stray {@code ?} at the end. This repair code will try
- * just deleting the illegal character.</td></tr>
- * </tbody>
- * </table>
- * <p>
- * If the cron expression parser returns an error that is not covered by any of these cases, or if
- * after {@code 5} attempts to fix the expression it still has problems, we give up.
- * </p>
- *
- * @since v7.0.0
- */
- public class CronExpressionFixer {
- private static final int MAX_ATTEMPTS = 5;
- private final String originalCronExpression;
- private final CronSyntaxException originalException;
- private String cronExpression;
- private CronSyntaxException exception;
- /**
- * Attempts to repair a possibly broken cron expression.
- *
- * @param cronExpression the cron expression to repair
- * @return the result of the attempted repair
- */
- @Nonnull
- public static Result repairCronExpression(final String cronExpression) {
- notNull("cronExpression", cronExpression);
- final CronSyntaxException exception = findNextSyntaxError(cronExpression);
- if (exception == null) {
- return Result.ALREADY_VALID;
- }
- return new CronExpressionFixer(cronExpression, exception).repair();
- }
- private CronExpressionFixer(@Nonnull final String originalCronExpression, @Nonnull final CronSyntaxException originalException) {
- this.originalCronExpression = originalCronExpression;
- this.originalException = originalException;
- }
- private Result repair() {
- cronExpression = originalCronExpression;
- exception = originalException;
- for (int attempt = 1; attempt <= MAX_ATTEMPTS && tryRepair(); ++attempt) {
- exception = findNextSyntaxError(cronExpression);
- if (exception == null) {
- return Result.success(cronExpression);
- }
- }
- return Result.failure(originalException);
- }
- private boolean tryRepair() {
- switch (exception.getErrorCode()) {
- case INVALID_STEP:
- case INVALID_STEP_SECOND_OR_MINUTE:
- case INVALID_STEP_HOUR:
- case INVALID_STEP_DAY_OF_MONTH:
- case INVALID_STEP_MONTH:
- case INVALID_STEP_DAY_OF_WEEK:
- return discardInvalidStep(exception.getErrorOffset() - 1);
- case COMMA_WITH_WEEKDAY_DOM:
- case UNEXPECTED_TOKEN_FLAG_W:
- return discardWeekdayFlags(exception.getErrorOffset());
- case ILLEGAL_CHARACTER:
- case ILLEGAL_CHARACTER_AFTER_HASH:
- case ILLEGAL_CHARACTER_AFTER_INTERVAL:
- case ILLEGAL_CHARACTER_AFTER_QM:
- return discardIllegalCharacter(exception.getErrorOffset());
- default:
- return false;
- }
- }
- private boolean discardInvalidStep(final int offset) {
- if (isOutOfBounds(offset) || cronExpression.charAt(offset) != '/') {
- return false;
- }
- deleteSubstring(offset, searchForwardForEndOfField(offset));
- return true;
- }
- private boolean discardWeekdayFlags(final int offset) {
- if (isOutOfBounds(offset)) {
- return false;
- }
- final int start = searchBackwardForStartOfField(offset);
- int stop = searchForwardForEndOfField(offset);
- final StringBuilder sb = new StringBuilder(cronExpression);
- for (int i = start; i < stop; ++i) {
- final char c = sb.charAt(i);
- if (c == 'w' || c == 'W') {
- sb.deleteCharAt(i);
- --i;
- --stop;
- }
- }
- cronExpression = sb.toString();
- return true;
- }
- private boolean discardIllegalCharacter(final int offset) {
- if (isOutOfBounds(offset)) {
- return false;
- }
- deleteSubstring(offset, offset + 1);
- return true;
- }
- private void deleteSubstring(final int offset, final int stop) {
- final StringBuilder sb = new StringBuilder(cronExpression.length());
- sb.append(cronExpression, 0, offset);
- if (stop < cronExpression.length()) {
- sb.append(cronExpression, stop, cronExpression.length());
- }
- cronExpression = sb.toString();
- }
- private int searchBackwardForStartOfField(final int offset) {
- int start = offset - 1;
- while (start >= 0 && !isSpace(cronExpression.charAt(start))) {
- --start;
- }
- return start + 1;
- }
- private int searchForwardForEndOfField(final int offset) {
- int stop = offset + 1;
- while (stop < cronExpression.length() && !isSpace(cronExpression.charAt(stop))) {
- ++stop;
- }
- return stop;
- }
- private boolean isOutOfBounds(int offset) {
- return offset < 0 || offset >= cronExpression.length();
- }
- private static boolean isSpace(char c) {
- return c == ' ' || c == '\t';
- }
- private static CronSyntaxException findNextSyntaxError(final String cronExpression) {
- try {
- CronExpressionParser.parse(cronExpression);
- return null;
- } catch (CronSyntaxException cse) {
- return cse;
- }
- }
- /**
- * Represents the result of an attempt to repair a cron expression.
- * <ul>
- * <li>If the repair is unnecessary, then both {@link #getNewCronExpression()} and
- * {@link #getCronSyntaxException()} will be {@link Optional#empty()}.</li>
- * <li>If the repair was needed and succeeds, then {@link #getNewCronExpression()} returns the repaired
- * cron expression and {@link #getCronSyntaxException()} is {@link Optional#empty()}.</li>
- * <li>If the cron expression is broken but cannot be repaired, then {@link #getCronSyntaxException()} returns
- * the first syntax error found and {@link #getNewCronExpression()} is {@link Optional#empty()}.</li>
- * </ul>
- */
- public static class Result {
- static final Result ALREADY_VALID = new Result(Optional.empty(), Optional.empty());
- private final Optional<String> newCronExpression;
- private final Optional<CronSyntaxException> exception;
- private Result(Optional<String> newCronExpression, Optional<CronSyntaxException> exception) {
- this.newCronExpression = newCronExpression;
- this.exception = exception;
- }
- /**
- * Returns the first syntax error found for the cron expression (if the repair failed)
- *
- * @return the first syntax error found for the cron expression (if the repair failed)
- */
- public Optional<CronSyntaxException> getCronSyntaxException() {
- return exception;
- }
- /**
- * Returns the repaired cron expression (if the repair succeeded).
- *
- * @return the repaired cron expression (if the repair succeeded).
- */
- public Optional<String> getNewCronExpression() {
- return newCronExpression;
- }
- static Result success(String newCronExpression) {
- return new Result(Optional.of(newCronExpression), Optional.empty());
- }
- static Result failure(CronSyntaxException exception) {
- return new Result(Optional.empty(), Optional.of(exception));
- }
- @Override
- public boolean equals(final Object o) {
- return this == o || (o instanceof Result && equals((Result) o));
- }
- private boolean equals(@Nonnull Result other) {
- return newCronExpression.equals(other.newCronExpression) && exception.equals(other.exception);
- }
- @Override
- public int hashCode() {
- return 31 * newCronExpression.hashCode() + exception.hashCode();
- }
- @Override
- public String toString() {
- return "Result[newCronExpression=" + newCronExpression + ", exception=" + exception + ']';
- }
- }
- }