import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;

import org.junit.Test;
import org.quartz.CronExpression;
import org.quartz.CronScheduleBuilder;

/**
 * Quartz CronExpression 정규식 처리를 이해하기 위한 테스트 코드
 * 
 * @author ihoneymon
 * @see http://quartz-scheduler.org/api/2.2.0/org/quartz/CronExpression.html
 */
public class QuartzCronExpressionTest {

    /**
     * Null은 예외발생
     */
    @Test(expected = IllegalArgumentException.class)
    public void testNullCronExpressionException() {
        assertThat(CronExpression.isValidExpression(null), is(false));
        fail();
    }

    /**
     * 빈값이나 cron 정규식 표현이 아니라면 실패
     */
    @Test
    public void testEmptyCronExpressionException() {
        assertThat(CronExpression.isValidExpression(""), is(false));
        assertThat(CronExpression.isValidExpression("1 1+2 1*2"), is(false));
        assertThat(CronExpression.isValidExpression("testWrongChronExpression"), is(false));
    }

    /**
     * Quartz CronExpression 중에 DayOfMonth와 DayOfWeek에 대한 확인 DayOfMonth와
     * DayOfWeek의 표현식에 대해서는 신경을 써야한다.
     * 
     * Second Minute Hour DayOfMonth Month DayOfWeek (optional)Year
     */
    @Test
    public void testDayOfMonthAndDayOfWeekCronExpression() {
        /**
         * same DayOfMonth & DayOfMonth is valid fail DayOfMonth & DayOfMonth is
         * all: false DayOfMonth & DayOfMonth is none: valid fail
         */
        assertThat(CronExpression.isValidExpression("* * * * * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * ? * ?"), is(false));

        /**
         * DayOfMonth is all and DayOfMonth is none: valid success DayOfMonth is
         * all and DayOfMonth is define: valid fail
         */
        assertThat(CronExpression.isValidExpression("* * * * * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * * * MON"), is(false));
        assertThat(CronExpression.isValidExpression("* * * * * 2"), is(false));
        assertThat(CronExpression.isValidExpression("* * * * * 2,5"), is(false));

        /**
         * DayOfMonth is none and DayOfMonth is all: valid success DayOfMonth
         * required none DayOfMonth DayOfMonth가 none(?)인 경우에는 DayOfMonth에 허용되는
         * 정규식은 대부분 통과
         */
        assertThat(CronExpression.isValidExpression("* * * ? * *"), is(true));
        assertThat(CronExpression.isValidExpression("* * * ? * MON"), is(true));
        assertThat(CronExpression.isValidExpression("* * * ? * MON-FRI"), is(true));
        assertThat(CronExpression.isValidExpression("* * * ? * MON-FRI,SUN"), is(true));
        assertThat(CronExpression.isValidExpression("* * * ? * MON#2"), is(true));
        assertThat(CronExpression.isValidExpression("* * * ? * 2#2"), is(true));
        assertThat(CronExpression.isValidExpression("* * * ? * 2#2,1#2"), is(false));

        /**
         * DayOfMonth에
         */
        assertThat(CronExpression.isValidExpression("* * * 1 * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * 1-3 * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * 1,6,9 * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * 1 * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * 1-3 * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * 1,6,9 * FRI"), is(false));
        /**
         * DayOfMonth use L(last) special character
         */
        assertThat(CronExpression.isValidExpression("* * * L * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * L * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * L * MON"), is(false));
        /**
         * DayOfMonth에 L은 단독으로 사용되거나 '-nth day'만 허용
         */
        assertThat(CronExpression.isValidExpression("* * * 15L * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * L15 * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * 15L * MON"), is(false));
        assertThat(CronExpression.isValidExpression("* * * 15L * ?"), is(false));
        assertThat(CronExpression.isValidExpression("* * * L-3 * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * L-3 * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * L-3 * MON"), is(false));

        /**
         * DayOfMonth use W(week: mon-fri) special character
         */
        assertThat(CronExpression.isValidExpression("* * * W * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * 15W * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * 15W * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * 15W,16W * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * 15W-17W * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * 15W/2W * ?"), is(true));

        /**
         * DayOfMonth use LW(last-week: mon-fri) special character
         */
        assertThat(CronExpression.isValidExpression("* * * LW * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * LW * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * 1LW * ?"), is(false));
        assertThat(CronExpression.isValidExpression("* * * LW2 * ?"), is(false));
        assertThat(CronExpression.isValidExpression("* * * LW * MON"), is(false));
        assertThat(CronExpression.isValidExpression("* * * LW * 2"), is(false));
    }

    @Test(expected = RuntimeException.class)
    public void testCronBuilder() {
        assertThat(CronExpression.isValidExpression("* * * LW * *"), is(false));
        CronScheduleBuilder.cronSchedule("* * * LW * *").build();
    }
}

CronExpression.isValidExpression() 에서 false가 발생한 정규식을 CronScheduleBuilder.cronSchedule()를 사용하여 스케줄을 생성하려하면 RuntimeException이 발생한다.

필드명 Field Name 허용값Allowed Value 허용특수문자 Allowed Special Chracters
Seconds 0-59 , - * /
Minutes 0-59 , - * /
Hours 0-23 , - * /
Day-of-month 1-31 , - * ? / L W
Month 1-12 or JAN-DEC , - * /
Day-of-Week 1-7 or SUN-SAT , - * ? / L #
Year (Optional) empty, 1970-2199 , - * /

허용특수문자

  • *: 정의된 모든 값을 사용, 분에 사용하면 매 분단위로.
  • ?: 지정된 값 없이 무작위로 진행되며, Day-of-month와 Day-of-Week에서만 사용하며, Day-of-month가 지정되었을 때 Day-of-Week의 적용을 배제하거나 반대의 경우 사용할 수 있겠다.
  • -: 범위를 지정할 때 사용, 10-12는 10, 11 그리고 12
  • ,: 특정값을 여러개 사용할 때 사용, MON,WED,FRI 는 월요일, 수요일, 금요일
  • /: 시작값/증가값의 형태로 사용하며 0/15인 경우 0, 15, 30, 45 실행되며, 5/15인 경우 5, 20, 35, 50실행됨
  • L: Day-of-month와 Day-of-Week에서만 사용하는 특수문자로, 마지막 값을 반환하는 용도로 사용된다.
    • Day-of-Week에서 사용하면 토요일(7=Saturday)이다.
    • Day-of-month에서 사용시에 ‘6L’인 경우에는 이번달 마지막주 금요일을 지정하는 것과 같다. ‘L-3’인 경우에는 마지막날에서 3일전을 의미한다.
  • W: 주간을 지정하며 15W로 정의하면 이번달 15일 실행되는지 여부를 확인하여 15일이 토요일인 경우에는 14일 금요일 실행되고, 15일이 일요일인 경우에는 16일 월요일 실행된다.
  • LW: ‘L’과 ‘W’를 결합해서 사용가능한데, Day-of-month 필드에 ‘LW’를 정의하면 매달 주간 마지막날에 실행이되는데, 마지막 날이 토요일인지 일요일인지에 따라서 ‘W’의 처리방법을 적용하게 된다.
  • #: Day-of-Week 에서만 허용되는 특수문자로 6#3의 경우는 3번째 금요일(“6”=Friday and “#3”은 3rd one in month)에 실행된다는 의미를 가진다.

정해진 문자와 월, 요일에 대해서는 대소문자를 가리지 않는다.

참고

+ Recent posts