From a902d958c15601f408d1c1d94cac710020823861 Mon Sep 17 00:00:00 2001 From: sirivarma Date: Sun, 9 Mar 2025 00:07:42 -0800 Subject: [PATCH 1/4] Add jobs --- pom.xml | 1 + sdk-jobs/pom.xml | 133 +++++++++ .../jobs/client/CronExpressionBuilder.java | 234 ++++++++++++++++ .../java/io/dapr/jobs/client/CronPeriod.java | 16 ++ .../java/io/dapr/jobs/client/DayOfWeek.java | 22 ++ .../io/dapr/jobs/client/JobsSchedule.java | 73 +++++ .../java/io/dapr/jobs/client/MonthOfYear.java | 27 ++ .../dapr/jobs/CronExpressionBuilderTest.java | 253 ++++++++++++++++++ 8 files changed, 759 insertions(+) create mode 100644 sdk-jobs/pom.xml create mode 100644 sdk-jobs/src/main/java/io/dapr/jobs/client/CronExpressionBuilder.java create mode 100644 sdk-jobs/src/main/java/io/dapr/jobs/client/CronPeriod.java create mode 100644 sdk-jobs/src/main/java/io/dapr/jobs/client/DayOfWeek.java create mode 100644 sdk-jobs/src/main/java/io/dapr/jobs/client/JobsSchedule.java create mode 100644 sdk-jobs/src/main/java/io/dapr/jobs/client/MonthOfYear.java create mode 100644 sdk-jobs/test/test/io/dapr/jobs/CronExpressionBuilderTest.java diff --git a/pom.xml b/pom.xml index bf727fe94d..90f9a0aed6 100644 --- a/pom.xml +++ b/pom.xml @@ -340,6 +340,7 @@ spring-boot-examples testcontainers-dapr + sdk-jobs diff --git a/sdk-jobs/pom.xml b/sdk-jobs/pom.xml new file mode 100644 index 0000000000..6f10b61412 --- /dev/null +++ b/sdk-jobs/pom.xml @@ -0,0 +1,133 @@ + + 4.0.0 + + + io.dapr + dapr-sdk-parent + 1.15.0-SNAPSHOT + + + dapr-sdk-jobs + jar + 0.15.0-SNAPSHOT + dapr-sdk-jobs + SDK for Jobs on Dapr + + + false + + + + + io.dapr + dapr-sdk + ${project.parent.version} + + + io.dapr + dapr-sdk-autogen + 1.15.0-SNAPSHOT + + + org.mockito + mockito-core + test + + + org.mockito + mockito-inline + 4.2.0 + test + + + org.junit.jupiter + junit-jupiter + test + + + org.junit.vintage + junit-vintage-engine + 5.7.0 + test + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.2.0 + + + attach-javadocs + + jar + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.11 + + + default-prepare-agent + + prepare-agent + + + + report + test + + report + + + target/jacoco-report/ + + + + check + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + 80% + + + + + + + + + + + + diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/CronExpressionBuilder.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/CronExpressionBuilder.java new file mode 100644 index 0000000000..00fd1b24eb --- /dev/null +++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/CronExpressionBuilder.java @@ -0,0 +1,234 @@ +package io.dapr.jobs.client; + +import java.util.ArrayList; +import java.util.List; + +public class CronExpressionBuilder { + + private final List seconds; + private final List minutes; + private final List hours; + private final List dayOfMonth; + private final List dayOfWeek; + private final List monthOfYear; + + public CronExpressionBuilder() { + this.seconds = new ArrayList<>(); + this.minutes = new ArrayList<>(); + this.hours = new ArrayList<>(); + this.dayOfMonth = new ArrayList<>(); + this.dayOfWeek = new ArrayList<>(); + this.monthOfYear = new ArrayList<>(); + } + + /** + * Convert to cron expression depending on period and values. + * + * @param cronPeriod {@link CronPeriod}. + * @param values for the cronPeriod. + * @return this. + */ + public CronExpressionBuilder add(CronPeriod cronPeriod, Integer... values) { + throwIfNull(cronPeriod); + throwIfNull(values); + + StringBuilder builder = new StringBuilder(); + for (Integer value: values) { + throwIfNull(value); + validatePeriod(cronPeriod, value); + builder.append(value).append(","); + } + + addToPeriod(cronPeriod, builder.deleteCharAt(builder.length() - 1).toString()); + + return this; + } + + public CronExpressionBuilder add(MonthOfYear... values) { + throwIfNull(values); + + StringBuilder builder = new StringBuilder(); + for (MonthOfYear value: values) { + throwIfNull(value); + builder.append(value).append(","); + } + + addToPeriod(CronPeriod.MonthOfYear, builder.deleteCharAt(builder.length() - 1).toString()); + + return this; + } + + public CronExpressionBuilder add(DayOfWeek... values) { + throwIfNull(values); + + StringBuilder builder = new StringBuilder(); + for (DayOfWeek value: values) { + throwIfNull(value); + builder.append(value).append(","); + } + + addToPeriod(CronPeriod.DayOfWeek, builder.deleteCharAt(builder.length() - 1).toString()); + + return this; + } + + public CronExpressionBuilder addRange(CronPeriod period, int from, int to) { + throwIfNull(period); + validateRange(from, to); + validatePeriod(period, from); + validatePeriod(period, to); + + addToPeriod(period, from + "-" + to); + + return this; + } + + public CronExpressionBuilder addRange(DayOfWeek from, DayOfWeek to) { + throwIfNull(from); + throwIfNull(to); + validateRange(from.getValue(), to.getValue()); + + addToPeriod(CronPeriod.DayOfWeek, from + "-" + to); + return this; + } + + public CronExpressionBuilder addRange(MonthOfYear from, MonthOfYear to) { + throwIfNull(from); + throwIfNull(to); + + addToPeriod(CronPeriod.MonthOfYear, from + "-" + to); + return this; + } + + public CronExpressionBuilder addStepRange(CronPeriod period, int from, int to, int denominator) { + throwIfNull(period); + validateRange(from, to); + validatePeriod(period, denominator); + + addToPeriod(period, from + "-" + to + "/" + denominator); + return this; + } + + public CronExpressionBuilder addStep(CronPeriod period, int numerator, int denominator) { + throwIfNull(period); + validatePeriod(period, numerator); + validatePeriod(period, denominator); + + addToPeriod(period, numerator + "/" + denominator); + return this; + } + + public CronExpressionBuilder addStep(CronPeriod period, int denominator) { + throwIfNull(period); + validatePeriod(period, denominator); + + addToPeriod(period, "*/" + denominator); + return this; + } + + public String build() { + + if (this.monthOfYear.isEmpty()) { + this.monthOfYear.add("*"); + } + + if (this.dayOfWeek.isEmpty()) { + this.dayOfWeek.add("*"); + } + + if (this.seconds.isEmpty()) { + this.seconds.add("*"); + } + + if (this.minutes.isEmpty()) { + this.minutes.add("*"); + } + + if (this.hours.isEmpty()) { + this.hours.add("*"); + } + + if (this.dayOfMonth.isEmpty()) { + this.dayOfMonth.add("*"); + } + + StringBuilder cronExpression = new StringBuilder(); + cronExpression.append(String.join(",", this.seconds)).append(" "); + cronExpression.append(String.join(",", this.minutes)).append(" "); + cronExpression.append(String.join(",", this.hours)).append(" "); + cronExpression.append(String.join(",", this.dayOfMonth)).append(" "); + cronExpression.append(String.join(",", this.monthOfYear)).append(" "); + cronExpression.append(String.join(",", this.dayOfWeek)); + + return cronExpression.toString(); + } + + private void validatePeriod(CronPeriod cronPeriod, int value) { + switch (cronPeriod) { + case SECONDS: + case MINUTES: + if (value < 0 || value > 59) { + throw new IllegalArgumentException(cronPeriod + " must be between [0, 59] inclusive"); + } + break; + case HOURS: + if (value < 0 || value > 23) { + throw new IllegalArgumentException(cronPeriod + " must be between [0, 23] inclusive"); + } + break; + case DayOfMonth: + if (value < 1 || value > 31) { + throw new IllegalArgumentException(cronPeriod + " must be between [1, 31] inclusive"); + } + break; + case MonthOfYear: + if (value < 1 || value > 12) { + throw new IllegalArgumentException(cronPeriod + " must be between [1, 12] inclusive"); + } + break; + case DayOfWeek: + if (value < 0 || value > 6) { + throw new IllegalArgumentException(cronPeriod + " must be between [0, 6] inclusive"); + } + break; + default: throw new IllegalArgumentException("Invalid CronPeriod: " + cronPeriod); + } + } + + private void validateRange(int from, int to) { + if (from > to || from == to) { + throw new IllegalArgumentException("from must be before to (from < to)"); + } + } + + private void addToPeriod(CronPeriod cronPeriod, String value) { + switch (cronPeriod) { + case SECONDS: + this.seconds.add(value); + break; + case MINUTES: + this.minutes.add(value); + break; + case HOURS: + this.hours.add(value); + break; + case DayOfMonth: + this.dayOfMonth.add(value); + break; + case MonthOfYear: + this.monthOfYear.add(value); + break; + case DayOfWeek: + this.dayOfWeek.add(value); + break; + default: + throw new IllegalArgumentException("Invalid CronPeriod: " + cronPeriod); + } + } + + private void throwIfNull(Object obj) { + if (obj == null) { + throw new IllegalArgumentException("None of the parameters can be null"); + } + } +} \ No newline at end of file diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/CronPeriod.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/CronPeriod.java new file mode 100644 index 0000000000..b58b8a6454 --- /dev/null +++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/CronPeriod.java @@ -0,0 +1,16 @@ +package io.dapr.jobs.client; + +public enum CronPeriod { + + SECONDS, + + MINUTES, + + HOURS, + + DayOfMonth, + + MonthOfYear, + + DayOfWeek, +} diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/DayOfWeek.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/DayOfWeek.java new file mode 100644 index 0000000000..ff3a1d705c --- /dev/null +++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/DayOfWeek.java @@ -0,0 +1,22 @@ +package io.dapr.jobs.client; + +public enum DayOfWeek { + + SUN(0), + MON(1), + TUE(2), + WED(3), + THU(4), + FRI(5), + SAT(6); + + private final int value; + + private DayOfWeek(int value) { + this.value = value; + } + + int getValue() { + return this.value; + } +} \ No newline at end of file diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/JobsSchedule.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/JobsSchedule.java new file mode 100644 index 0000000000..b602b92886 --- /dev/null +++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/JobsSchedule.java @@ -0,0 +1,73 @@ +package io.dapr.jobs.client; + +import java.time.Duration; + +public class JobsSchedule { + + private final String expression; + + private JobsSchedule(String expression) { + this.expression = expression; + } + + public static JobsSchedule fromPeriod(Duration duration) { + if (duration == null) { + throw new IllegalArgumentException("duration cannot be null"); + } + + String formattedDuration = String.format("%dh%dm%ds%dms", + duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart(), duration.toMillisPart()); + return new JobsSchedule("@every " + formattedDuration); + } + + public static JobsSchedule fromString(String cronExpression) { + return new JobsSchedule(cronExpression); + } + + public static JobsSchedule yearly() { + return new JobsSchedule(new CronExpressionBuilder() + .add(CronPeriod.SECONDS, 0) + .add(CronPeriod.MINUTES, 0) + .add(CronPeriod.HOURS, 0) + .add(CronPeriod.DayOfMonth, 1) + .add(CronPeriod.MonthOfYear, 1) + .build()); + } + + public static JobsSchedule monthly() { + return new JobsSchedule(new CronExpressionBuilder() + .add(CronPeriod.SECONDS, 0) + .add(CronPeriod.MINUTES, 0) + .add(CronPeriod.HOURS, 0) + .add(CronPeriod.DayOfMonth, 1) + .build()); + } + + public static JobsSchedule weekly() { + return new JobsSchedule(new CronExpressionBuilder() + .add(CronPeriod.SECONDS, 0) + .add(CronPeriod.MINUTES, 0) + .add(CronPeriod.HOURS, 0) + .add(CronPeriod.DayOfWeek, 0) + .build()); + } + + public static JobsSchedule daily() { + return new JobsSchedule(new CronExpressionBuilder() + .add(CronPeriod.SECONDS, 0) + .add(CronPeriod.MINUTES, 0) + .add(CronPeriod.HOURS, 0) + .build()); + } + + public static JobsSchedule hourly() { + return new JobsSchedule(new CronExpressionBuilder() + .add(CronPeriod.SECONDS, 0) + .add(CronPeriod.MINUTES, 0) + .build()); + } + + public String getExpression() { + return this.expression; + } +} diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/MonthOfYear.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/MonthOfYear.java new file mode 100644 index 0000000000..d9def02d68 --- /dev/null +++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/MonthOfYear.java @@ -0,0 +1,27 @@ +package io.dapr.jobs.client; + +public enum MonthOfYear { + + JAN(1), + FEB(2), + MAR(3), + APR(4), + MAY(5), + JUN(6), + JUL(7), + AUG(8), + SEP(9), + OCT(10), + NOV(11), + DEC(12); + + private final int value; + + MonthOfYear(int value) { + this.value = value; + } + + int getValue() { + return this.value; + } +} \ No newline at end of file diff --git a/sdk-jobs/test/test/io/dapr/jobs/CronExpressionBuilderTest.java b/sdk-jobs/test/test/io/dapr/jobs/CronExpressionBuilderTest.java new file mode 100644 index 0000000000..6a30925355 --- /dev/null +++ b/sdk-jobs/test/test/io/dapr/jobs/CronExpressionBuilderTest.java @@ -0,0 +1,253 @@ +package io.dapr.jobs; + +import io.dapr.jobs.client.CronExpressionBuilder; +import io.dapr.jobs.client.CronPeriod; +import io.dapr.jobs.client.DayOfWeek; +import io.dapr.jobs.client.MonthOfYear; +import org.junit.Assert; +import org.junit.Test; + +public class CronExpressionBuilderTest { + + @Test + public void builderWithoutParametersShouldReturnDefaultValues() { + String cronExpression = new CronExpressionBuilder().build(); + Assert.assertEquals("* * * * * *", cronExpression); + } + + @Test + public void builderWithInvalidSecondsShouldThrowIllegalArgumentException() { + IllegalArgumentException exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .add(CronPeriod.SECONDS, 60).build()); + Assert.assertTrue(exception.getMessage().contains("SECONDS must be between [0, 59]")); + exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .add(CronPeriod.SECONDS, -1).build()); + Assert.assertTrue(exception.getMessage().contains("SECONDS must be between [0, 59]")); + } + + @Test + public void builderWithInvalidMinutesShouldThrowIllegalArgumentException() { + IllegalArgumentException exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .add(CronPeriod.MINUTES, 60).build()); + Assert.assertTrue(exception.getMessage().contains("MINUTES must be between [0, 59]")); + exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .add(CronPeriod.MINUTES, -1).build()); + Assert.assertTrue(exception.getMessage().contains("MINUTES must be between [0, 59]")); + } + + @Test + public void builderWithInvalidHoursShouldThrowIllegalArgumentException() { + IllegalArgumentException exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .add(CronPeriod.HOURS, -1).build()); + Assert.assertTrue(exception.getMessage().contains("HOURS must be between [0, 23]")); + exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .add(CronPeriod.HOURS, 24).build()); + Assert.assertTrue(exception.getMessage().contains("HOURS must be between [0, 23]")); + } + + @Test + public void builderWithInvalidDayOfMonthShouldThrowIllegalArgumentException() { + IllegalArgumentException exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .add(CronPeriod.DayOfMonth, 32).build()); + Assert.assertTrue(exception.getMessage().contains("DayOfMonth must be between [1, 31]")); + exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .add(CronPeriod.DayOfMonth, 0).build()); + Assert.assertTrue(exception.getMessage().contains("DayOfMonth must be between [1, 31]")); + } + + @Test + public void builderWithInvalidMonthOfYearShouldThrowIllegalArgumentException() { + IllegalArgumentException exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .add(CronPeriod.MonthOfYear, 0).build()); + Assert.assertTrue(exception.getMessage().contains("MonthOfYear must be between [1, 12]")); + exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .add(CronPeriod.MonthOfYear, 13).build()); + Assert.assertTrue(exception.getMessage().contains("MonthOfYear must be between [1, 12]")); + } + + @Test + public void builderWithInvalidDayOfWeekShouldThrowIllegalArgumentException() { + IllegalArgumentException exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .add(CronPeriod.DayOfWeek, -1).build()); + Assert.assertTrue(exception.getMessage().contains("DayOfWeek must be between [0, 6]")); + exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .add(CronPeriod.DayOfWeek, 7).build()); + Assert.assertTrue(exception.getMessage().contains("DayOfWeek must be between [0, 6]")); + } + + @Test + public void builderWithSecondsShouldReturnWithOnlySecondsSet() { + String cronExpression = new CronExpressionBuilder() + .add(CronPeriod.SECONDS, 5) + .build(); + Assert.assertEquals("5 * * * * *", cronExpression); + } + + @Test + public void builderWithMultipleCallsToAddSecondsShouldReturnWithMultipleValues() { + String cronExpression = new CronExpressionBuilder() + .add(CronPeriod.SECONDS, 5) + .add(CronPeriod.SECONDS, 10) + .add(CronPeriod.SECONDS, 20) + .build(); + Assert.assertEquals("5,10,20 * * * * *", cronExpression); + } + + @Test + public void builderWithCallToAddRangeForSecondShouldReturnCorrectValues() { + String cronExpression = new CronExpressionBuilder() + .add(CronPeriod.SECONDS, 5) + .add(CronPeriod.SECONDS, 10) + .addRange(CronPeriod.SECONDS, 40, 50) + .build(); + Assert.assertEquals("5,10,40-50 * * * * *", cronExpression); + } + + @Test + public void builderWithCallToAddStepForSecondShouldReturnCorrectValues() { + String cronExpression = new CronExpressionBuilder() + .add(CronPeriod.SECONDS, 5) + .addStep(CronPeriod.SECONDS, 10) + .addRange(CronPeriod.SECONDS, 40, 50) + .build(); + Assert.assertEquals("5,*/10,40-50 * * * * *", cronExpression); + } + + @Test + public void builderWithMinutesShouldReturnWithOnlySecondsSet() { + String cronExpression = new CronExpressionBuilder() + .add(CronPeriod.MINUTES, 5) + .build(); + Assert.assertEquals("* 5 * * * *", cronExpression); + } + + @Test + public void builderWithMultipleCallsToAddMinutesShouldReturnWithMultipleValues() { + String cronExpression = new CronExpressionBuilder() + .add(CronPeriod.MINUTES, 5) + .add(CronPeriod.MINUTES, 10) + .add(CronPeriod.MINUTES, 20) + .build(); + Assert.assertEquals("* 5,10,20 * * * *", cronExpression); + } + + @Test + public void builderWithCallToAddRangeForMinutesShouldReturnCorrectValues() { + String cronExpression = new CronExpressionBuilder() + .add(CronPeriod.MINUTES, 5) + .add(CronPeriod.MINUTES, 10) + .addRange(CronPeriod.MINUTES, 40, 50) + .build(); + Assert.assertEquals("* 5,10,40-50 * * * *", cronExpression); + } + + @Test + public void builderWithCallToAddStepForMinutesShouldReturnCorrectValues() { + String cronExpression = new CronExpressionBuilder() + .add(CronPeriod.MINUTES, 5) + .addStep(CronPeriod.MINUTES, 10) + .addRange(CronPeriod.MINUTES, 40, 50) + .build(); + Assert.assertEquals("* 5,*/10,40-50 * * * *", cronExpression); + } + + @Test + public void builderWithCallToAddForSecondsAndMinutesShouldReturnCorrectValues() { + String cronExpression = new CronExpressionBuilder() + .add(CronPeriod.SECONDS, 2) + .add(CronPeriod.MINUTES, 5) + .addStep(CronPeriod.MINUTES, 10) + .addRange(CronPeriod.MINUTES, 40, 50) + .build(); + Assert.assertEquals("2 5,*/10,40-50 * * * *", cronExpression); + } + + @Test + public void builderWithHoursShouldReturnWithOnlySecondsSet() { + String cronExpression = new CronExpressionBuilder() + .add(CronPeriod.HOURS, 5) + .build(); + Assert.assertEquals("* * 5 * * *", cronExpression); + } + + @Test + public void builderWithMultipleCallsToAddHoursShouldReturnWithMultipleValues() { + String cronExpression = new CronExpressionBuilder() + .add(CronPeriod.HOURS, 5) + .add(CronPeriod.HOURS, 10) + .add(CronPeriod.HOURS, 20) + .build(); + Assert.assertEquals("* * 5,10,20 * * *", cronExpression); + } + + @Test + public void builderWithCallToAddRangeForHoursShouldReturnCorrectValues() { + String cronExpression = new CronExpressionBuilder() + .add(CronPeriod.HOURS, 5) + .add(CronPeriod.HOURS, 10) + .addRange(CronPeriod.HOURS, 11, 12) + .build(); + Assert.assertEquals("* * 5,10,11-12 * * *", cronExpression); + } + + @Test + public void builderWithCallToAddStepForHoursShouldReturnCorrectValues() { + String cronExpression = new CronExpressionBuilder() + .add(CronPeriod.HOURS, 5) + .addStep(CronPeriod.HOURS, 10) + .addRange(CronPeriod.HOURS, 13, 14) + .build(); + Assert.assertEquals("* * 5,*/10,13-14 * * *", cronExpression); + } + + @Test + public void builderWithCallToAddForSecondsMinutesHoursShouldReturnCorrectValues() { + String cronExpression = new CronExpressionBuilder() + .add(CronPeriod.SECONDS, 2) + .add(CronPeriod.MINUTES, 5) + .addStep(CronPeriod.MINUTES, 10) + .addRange(CronPeriod.MINUTES, 40, 50) + .add(CronPeriod.HOURS, 20) + .addRange(CronPeriod.HOURS, 1, 2) + .addStep(CronPeriod.HOURS, 4) + .addStepRange(CronPeriod.HOURS, 5, 6, 3) + .build(); + Assert.assertEquals("2 5,*/10,40-50 20,1-2,*/4,5-6/3 * * *", cronExpression); + } + + @Test + public void builderWithCallToAddForMonthOfDayAndDayOfWeekShouldReturnCorrectValues() { + String cronExpression = new CronExpressionBuilder() + .add(MonthOfYear.JAN, MonthOfYear.FEB) + .add(DayOfWeek.MON, DayOfWeek.THU) + .add(CronPeriod.SECONDS, 1,2,3) + .add(CronPeriod.MINUTES, 20,30) + .build(); + Assert.assertEquals("1,2,3 20,30 * * JAN,FEB MON,THU", cronExpression); + } + + @Test + public void builderWithCallToAddForRangeMonthOfDayAndDayOfWeekShouldReturnCorrectValues() { + String cronExpression = new CronExpressionBuilder() + .add(MonthOfYear.JAN, MonthOfYear.FEB) + .add(DayOfWeek.MON, DayOfWeek.THU) + .add(CronPeriod.SECONDS, 1,2,3) + .add(CronPeriod.MINUTES, 20,30) + .addRange(MonthOfYear.MAR, MonthOfYear.APR) + .addRange(DayOfWeek.SUN, DayOfWeek.MON) + .build(); + Assert.assertEquals("1,2,3 20,30 * * JAN,FEB,MAR-APR MON,THU,SUN-MON", cronExpression); + } +} From d431b2a288459cd0a36f8359b19428cd01304772 Mon Sep 17 00:00:00 2001 From: sirivarma Date: Sun, 9 Mar 2025 08:52:36 -0700 Subject: [PATCH 2/4] Add validations --- .../jobs/client/CronExpressionBuilder.java | 22 ++-- .../dapr/jobs/CronExpressionBuilderTest.java | 114 ++++++++++++++---- .../test/io/dapr/jobs/JobsScheduleTest.java | 68 +++++++++++ 3 files changed, 172 insertions(+), 32 deletions(-) create mode 100644 sdk-jobs/test/test/io/dapr/jobs/JobsScheduleTest.java diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/CronExpressionBuilder.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/CronExpressionBuilder.java index 00fd1b24eb..c8a53ee464 100644 --- a/sdk-jobs/src/main/java/io/dapr/jobs/client/CronExpressionBuilder.java +++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/CronExpressionBuilder.java @@ -100,29 +100,29 @@ public CronExpressionBuilder addRange(MonthOfYear from, MonthOfYear to) { return this; } - public CronExpressionBuilder addStepRange(CronPeriod period, int from, int to, int denominator) { + public CronExpressionBuilder addStepRange(CronPeriod period, int from, int to, int interval) { throwIfNull(period); validateRange(from, to); - validatePeriod(period, denominator); + validatePeriod(period, interval); - addToPeriod(period, from + "-" + to + "/" + denominator); + addToPeriod(period, from + "-" + to + "/" + interval); return this; } - public CronExpressionBuilder addStep(CronPeriod period, int numerator, int denominator) { + public CronExpressionBuilder addStep(CronPeriod period, int numerator, int interval) { throwIfNull(period); validatePeriod(period, numerator); - validatePeriod(period, denominator); + validatePeriod(period, interval); - addToPeriod(period, numerator + "/" + denominator); + addToPeriod(period, numerator + "/" + interval); return this; } - public CronExpressionBuilder addStep(CronPeriod period, int denominator) { + public CronExpressionBuilder addStep(CronPeriod period, int interval) { throwIfNull(period); - validatePeriod(period, denominator); + validatePeriod(period, interval); - addToPeriod(period, "*/" + denominator); + addToPeriod(period, "*/" + interval); return this; } @@ -197,7 +197,7 @@ private void validatePeriod(CronPeriod cronPeriod, int value) { private void validateRange(int from, int to) { if (from > to || from == to) { - throw new IllegalArgumentException("from must be before to (from < to)"); + throw new IllegalArgumentException("from must be less than to (from < to)"); } } @@ -228,7 +228,7 @@ private void addToPeriod(CronPeriod cronPeriod, String value) { private void throwIfNull(Object obj) { if (obj == null) { - throw new IllegalArgumentException("None of the parameters can be null"); + throw new IllegalArgumentException("None of the input parameters can be null"); } } } \ No newline at end of file diff --git a/sdk-jobs/test/test/io/dapr/jobs/CronExpressionBuilderTest.java b/sdk-jobs/test/test/io/dapr/jobs/CronExpressionBuilderTest.java index 6a30925355..3ee235d9c0 100644 --- a/sdk-jobs/test/test/io/dapr/jobs/CronExpressionBuilderTest.java +++ b/sdk-jobs/test/test/io/dapr/jobs/CronExpressionBuilderTest.java @@ -6,13 +6,17 @@ import io.dapr.jobs.client.MonthOfYear; import org.junit.Assert; import org.junit.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.Assert.assertEquals; public class CronExpressionBuilderTest { @Test public void builderWithoutParametersShouldReturnDefaultValues() { String cronExpression = new CronExpressionBuilder().build(); - Assert.assertEquals("* * * * * *", cronExpression); + assertEquals("* * * * * *", cronExpression); } @Test @@ -52,14 +56,18 @@ public void builderWithInvalidHoursShouldThrowIllegalArgumentException() { } @Test - public void builderWithInvalidDayOfMonthShouldThrowIllegalArgumentException() { + public void builderWithInvalidDayOfMonthShouldThrowIllegalArgumentException1() { IllegalArgumentException exception = Assert.assertThrows(IllegalArgumentException.class, () -> new CronExpressionBuilder() - .add(CronPeriod.DayOfMonth, 32).build()); + .add(CronPeriod.DayOfMonth, 0).build()); Assert.assertTrue(exception.getMessage().contains("DayOfMonth must be between [1, 31]")); - exception = Assert.assertThrows(IllegalArgumentException.class, + } + + @Test + public void builderWithInvalidDayOfMonthShouldThrowIllegalArgumentException() { + IllegalArgumentException exception = Assert.assertThrows(IllegalArgumentException.class, () -> new CronExpressionBuilder() - .add(CronPeriod.DayOfMonth, 0).build()); + .add(CronPeriod.DayOfMonth, 32).build()); Assert.assertTrue(exception.getMessage().contains("DayOfMonth must be between [1, 31]")); } @@ -87,12 +95,66 @@ public void builderWithInvalidDayOfWeekShouldThrowIllegalArgumentException() { Assert.assertTrue(exception.getMessage().contains("DayOfWeek must be between [0, 6]")); } + @Test + public void builderWithInvalidRangeShouldThrowIllegalArgumentException() { + IllegalArgumentException exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .addRange(CronPeriod.HOURS, 20, 19).build()); + Assert.assertTrue(exception.getMessage().contains("from must be less than to")); + } + + @Test + public void builderWithInvalidMinuteRangeSpecifiedShouldThrowIllegalArgumentException() { + IllegalArgumentException exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .addRange(CronPeriod.MINUTES, 1, 1).build()); + Assert.assertTrue(exception.getMessage().contains("from must be less than to (from < to)")); + } + + @Test + public void builderWithInvalidParametersShouldThrowIllegalArgumentException() { + IllegalArgumentException exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .addRange(null, 1, 2).build()); + Assert.assertTrue(exception.getMessage().contains("None of the input parameters can be null")); + } + + @ParameterizedTest + @CsvSource({ + "SECONDS, 0, '0 * * * * *'", + "MINUTES, 30, '* 30 * * * *'", + "HOURS, 12, '* * 12 * * *'", + "DayOfMonth, 15, '* * * 15 * *'", + "MonthOfYear, 6, '* * * * 6 *'", + "DayOfWeek, 3, '* * * * * 3'" + }) + void testAddSingleValue(CronPeriod period, int value, String expected) { + CronExpressionBuilder builder = new CronExpressionBuilder().add(period, value); + assertEquals(expected, builder.build()); + } + + @Test + public void builderWithInvalidParameterInValuesShouldThrowIllegalArgumentException() { + IllegalArgumentException exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .add(null, MonthOfYear.JAN, null, MonthOfYear.FEB).build()); + Assert.assertTrue(exception.getMessage().contains("None of the input parameters can be null")); + } + + @Test + public void builderWithInvalidParameterInDayOfWeekValuesShouldThrowIllegalArgumentException() { + IllegalArgumentException exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .add(null, DayOfWeek.MON, null, DayOfWeek.THU).build()); + Assert.assertTrue(exception.getMessage().contains("None of the input parameters can be null")); + } + @Test public void builderWithSecondsShouldReturnWithOnlySecondsSet() { String cronExpression = new CronExpressionBuilder() .add(CronPeriod.SECONDS, 5) .build(); - Assert.assertEquals("5 * * * * *", cronExpression); + assertEquals("5 * * * * *", cronExpression); } @Test @@ -102,7 +164,7 @@ public void builderWithMultipleCallsToAddSecondsShouldReturnWithMultipleValues() .add(CronPeriod.SECONDS, 10) .add(CronPeriod.SECONDS, 20) .build(); - Assert.assertEquals("5,10,20 * * * * *", cronExpression); + assertEquals("5,10,20 * * * * *", cronExpression); } @Test @@ -112,7 +174,7 @@ public void builderWithCallToAddRangeForSecondShouldReturnCorrectValues() { .add(CronPeriod.SECONDS, 10) .addRange(CronPeriod.SECONDS, 40, 50) .build(); - Assert.assertEquals("5,10,40-50 * * * * *", cronExpression); + assertEquals("5,10,40-50 * * * * *", cronExpression); } @Test @@ -122,7 +184,7 @@ public void builderWithCallToAddStepForSecondShouldReturnCorrectValues() { .addStep(CronPeriod.SECONDS, 10) .addRange(CronPeriod.SECONDS, 40, 50) .build(); - Assert.assertEquals("5,*/10,40-50 * * * * *", cronExpression); + assertEquals("5,*/10,40-50 * * * * *", cronExpression); } @Test @@ -130,7 +192,7 @@ public void builderWithMinutesShouldReturnWithOnlySecondsSet() { String cronExpression = new CronExpressionBuilder() .add(CronPeriod.MINUTES, 5) .build(); - Assert.assertEquals("* 5 * * * *", cronExpression); + assertEquals("* 5 * * * *", cronExpression); } @Test @@ -140,7 +202,7 @@ public void builderWithMultipleCallsToAddMinutesShouldReturnWithMultipleValues() .add(CronPeriod.MINUTES, 10) .add(CronPeriod.MINUTES, 20) .build(); - Assert.assertEquals("* 5,10,20 * * * *", cronExpression); + assertEquals("* 5,10,20 * * * *", cronExpression); } @Test @@ -150,7 +212,7 @@ public void builderWithCallToAddRangeForMinutesShouldReturnCorrectValues() { .add(CronPeriod.MINUTES, 10) .addRange(CronPeriod.MINUTES, 40, 50) .build(); - Assert.assertEquals("* 5,10,40-50 * * * *", cronExpression); + assertEquals("* 5,10,40-50 * * * *", cronExpression); } @Test @@ -160,7 +222,7 @@ public void builderWithCallToAddStepForMinutesShouldReturnCorrectValues() { .addStep(CronPeriod.MINUTES, 10) .addRange(CronPeriod.MINUTES, 40, 50) .build(); - Assert.assertEquals("* 5,*/10,40-50 * * * *", cronExpression); + assertEquals("* 5,*/10,40-50 * * * *", cronExpression); } @Test @@ -171,7 +233,7 @@ public void builderWithCallToAddForSecondsAndMinutesShouldReturnCorrectValues() .addStep(CronPeriod.MINUTES, 10) .addRange(CronPeriod.MINUTES, 40, 50) .build(); - Assert.assertEquals("2 5,*/10,40-50 * * * *", cronExpression); + assertEquals("2 5,*/10,40-50 * * * *", cronExpression); } @Test @@ -179,7 +241,7 @@ public void builderWithHoursShouldReturnWithOnlySecondsSet() { String cronExpression = new CronExpressionBuilder() .add(CronPeriod.HOURS, 5) .build(); - Assert.assertEquals("* * 5 * * *", cronExpression); + assertEquals("* * 5 * * *", cronExpression); } @Test @@ -189,7 +251,7 @@ public void builderWithMultipleCallsToAddHoursShouldReturnWithMultipleValues() { .add(CronPeriod.HOURS, 10) .add(CronPeriod.HOURS, 20) .build(); - Assert.assertEquals("* * 5,10,20 * * *", cronExpression); + assertEquals("* * 5,10,20 * * *", cronExpression); } @Test @@ -199,7 +261,7 @@ public void builderWithCallToAddRangeForHoursShouldReturnCorrectValues() { .add(CronPeriod.HOURS, 10) .addRange(CronPeriod.HOURS, 11, 12) .build(); - Assert.assertEquals("* * 5,10,11-12 * * *", cronExpression); + assertEquals("* * 5,10,11-12 * * *", cronExpression); } @Test @@ -209,7 +271,7 @@ public void builderWithCallToAddStepForHoursShouldReturnCorrectValues() { .addStep(CronPeriod.HOURS, 10) .addRange(CronPeriod.HOURS, 13, 14) .build(); - Assert.assertEquals("* * 5,*/10,13-14 * * *", cronExpression); + assertEquals("* * 5,*/10,13-14 * * *", cronExpression); } @Test @@ -224,7 +286,7 @@ public void builderWithCallToAddForSecondsMinutesHoursShouldReturnCorrectValues( .addStep(CronPeriod.HOURS, 4) .addStepRange(CronPeriod.HOURS, 5, 6, 3) .build(); - Assert.assertEquals("2 5,*/10,40-50 20,1-2,*/4,5-6/3 * * *", cronExpression); + assertEquals("2 5,*/10,40-50 20,1-2,*/4,5-6/3 * * *", cronExpression); } @Test @@ -235,7 +297,7 @@ public void builderWithCallToAddForMonthOfDayAndDayOfWeekShouldReturnCorrectValu .add(CronPeriod.SECONDS, 1,2,3) .add(CronPeriod.MINUTES, 20,30) .build(); - Assert.assertEquals("1,2,3 20,30 * * JAN,FEB MON,THU", cronExpression); + assertEquals("1,2,3 20,30 * * JAN,FEB MON,THU", cronExpression); } @Test @@ -248,6 +310,16 @@ public void builderWithCallToAddForRangeMonthOfDayAndDayOfWeekShouldReturnCorrec .addRange(MonthOfYear.MAR, MonthOfYear.APR) .addRange(DayOfWeek.SUN, DayOfWeek.MON) .build(); - Assert.assertEquals("1,2,3 20,30 * * JAN,FEB,MAR-APR MON,THU,SUN-MON", cronExpression); + assertEquals("1,2,3 20,30 * * JAN,FEB,MAR-APR MON,THU,SUN-MON", cronExpression); + } + + @Test + public void builderWithCallToAddForStepShouldReturnCorrectValues() { + String cronExpression = new CronExpressionBuilder() + .add(MonthOfYear.JAN, MonthOfYear.FEB) + .add(DayOfWeek.MON, DayOfWeek.THU) + .addStep(CronPeriod.HOURS, 20, 2) + .build(); + assertEquals("* * 20/2 * JAN,FEB MON,THU", cronExpression); } } diff --git a/sdk-jobs/test/test/io/dapr/jobs/JobsScheduleTest.java b/sdk-jobs/test/test/io/dapr/jobs/JobsScheduleTest.java new file mode 100644 index 0000000000..b37b372cb2 --- /dev/null +++ b/sdk-jobs/test/test/io/dapr/jobs/JobsScheduleTest.java @@ -0,0 +1,68 @@ +package io.dapr.jobs.client; + +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; + +class JobsScheduleTest { + + @Test + void testFromPeriodValidDuration() { + Duration duration = Duration.ofHours(1).plusMinutes(30) + .plusSeconds(15).plusMillis(500); + JobsSchedule schedule = JobsSchedule.fromPeriod(duration); + assertEquals("@every 1h30m15s500ms", schedule.getExpression()); + } + + @Test + void testFromPeriodValidDurationWithoutSecondsAndMillSeconds() { + Duration duration = Duration.ofHours(1).plusMinutes(30); + JobsSchedule schedule = JobsSchedule.fromPeriod(duration); + assertEquals("@every 1h30m0s0ms", schedule.getExpression()); + } + + @Test + void testFromPeriodNullDuration() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> JobsSchedule.fromPeriod(null)); + assertEquals("duration cannot be null", exception.getMessage()); + } + + @Test + void testFromString() { + String cronExpression = "0 0 * * *"; + JobsSchedule schedule = JobsSchedule.fromString(cronExpression); + assertEquals(cronExpression, schedule.getExpression()); + } + + @Test + void testYearly() { + JobsSchedule schedule = JobsSchedule.yearly(); + assertEquals("0 0 0 1 1 *", schedule.getExpression()); + } + + @Test + void testMonthly() { + JobsSchedule schedule = JobsSchedule.monthly(); + assertEquals("0 0 0 1 * *", schedule.getExpression()); + } + + @Test + void testWeekly() { + JobsSchedule schedule = JobsSchedule.weekly(); + assertEquals("0 0 0 * * 0", schedule.getExpression()); + } + + @Test + void testDaily() { + JobsSchedule schedule = JobsSchedule.daily(); + assertEquals("0 0 0 * * *", schedule.getExpression()); + } + + @Test + void testHourly() { + JobsSchedule schedule = JobsSchedule.hourly(); + assertEquals("0 0 * * * *", schedule.getExpression()); + } +} From 0d6cb367331099311be8c36b4f1229f0362c3c46 Mon Sep 17 00:00:00 2001 From: sirivarma Date: Sun, 9 Mar 2025 13:08:26 -0700 Subject: [PATCH 3/4] Add things --- .../jobs/client/CronExpressionBuilder.java | 295 +++++++++++++----- .../java/io/dapr/jobs/client/CronPeriod.java | 32 +- .../java/io/dapr/jobs/client/DayOfWeek.java | 27 +- .../io/dapr/jobs/client/JobsSchedule.java | 69 ++++ .../java/io/dapr/jobs/client/MonthOfYear.java | 17 +- .../java/io/dapr/jobs/client/OrdinalEnum.java | 11 + .../dapr/jobs/CronExpressionBuilderTest.java | 23 ++ 7 files changed, 385 insertions(+), 89 deletions(-) create mode 100644 sdk-jobs/src/main/java/io/dapr/jobs/client/OrdinalEnum.java diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/CronExpressionBuilder.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/CronExpressionBuilder.java index c8a53ee464..d8c633186a 100644 --- a/sdk-jobs/src/main/java/io/dapr/jobs/client/CronExpressionBuilder.java +++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/CronExpressionBuilder.java @@ -1,8 +1,24 @@ package io.dapr.jobs.client; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +/** + * A builder class for constructing cron expressions. This class provides an easy way to construct cron expressions + * by adding individual values or ranges for each of the cron fields: seconds, minutes, hours, day of month, + * day of week, and month of year. It supports adding steps and ranges for fields where appropriate. + *

+ * Example usage: + *

+ * CronExpressionBuilder builder = new CronExpressionBuilder();
+ * builder.add(CronPeriod.MINUTES, 0, 15, 30); // Every 15 minutes starting at 0
+ * builder.add(CronPeriod.HOURS, 12); // At noon
+ * builder.addRange(CronPeriod.DAYOFMONTH, 1, 31); // On the 1st through the 31st day of the month
+ * builder.addStep(CronPeriod.MINUTES, 5); // Every 5 minutes
+ * System.out.println(builder.build()); // Outputs the cron expression
+ * 
+ */ public class CronExpressionBuilder { private final List seconds; @@ -12,6 +28,9 @@ public class CronExpressionBuilder { private final List dayOfWeek; private final List monthOfYear; + /** + * Constructs a new {@link CronExpressionBuilder} instance with empty cron fields. + */ public CronExpressionBuilder() { this.seconds = new ArrayList<>(); this.minutes = new ArrayList<>(); @@ -22,110 +41,121 @@ public CronExpressionBuilder() { } /** - * Convert to cron expression depending on period and values. + * Adds values to the specified cron period (e.g., minutes, hours, etc.). + * example: + * builder.add(CronPeriod.MINUTES, 0, 15, 30); // Adds 0, 15, and 30 minutes to the cron expression + * + * @param cronPeriod The cron period to modify (e.g., {CronPeriod.MINUTES}). + * @param values The values to be added to the cron period. + * @return The {@link CronExpressionBuilder} instance for method chaining. + * @throws IllegalArgumentException if values are invalid or empty. * - * @param cronPeriod {@link CronPeriod}. - * @param values for the cronPeriod. - * @return this. */ - public CronExpressionBuilder add(CronPeriod cronPeriod, Integer... values) { - throwIfNull(cronPeriod); - throwIfNull(values); - - StringBuilder builder = new StringBuilder(); - for (Integer value: values) { - throwIfNull(value); - validatePeriod(cronPeriod, value); - builder.append(value).append(","); - } - - addToPeriod(cronPeriod, builder.deleteCharAt(builder.length() - 1).toString()); - - return this; + public CronExpressionBuilder add(CronPeriod cronPeriod, int... values) { + return addInternal(cronPeriod, Arrays.stream(values).boxed().toArray()); } + /** + * Adds values for the {@link MonthOfYear} cron period. + * + * @param values The {@link MonthOfYear} values to be added. + * @return The {@link CronExpressionBuilder} instance for method chaining. + */ public CronExpressionBuilder add(MonthOfYear... values) { - throwIfNull(values); - - StringBuilder builder = new StringBuilder(); - for (MonthOfYear value: values) { - throwIfNull(value); - builder.append(value).append(","); - } - - addToPeriod(CronPeriod.MonthOfYear, builder.deleteCharAt(builder.length() - 1).toString()); - - return this; + return addInternal(CronPeriod.MonthOfYear, values); } + /** + * Adds values for the {@link DayOfWeek} cron period. + * + * @param values The {@link DayOfWeek} values to be added. + * @return The {@link CronExpressionBuilder} instance for method chaining. + */ public CronExpressionBuilder add(DayOfWeek... values) { - throwIfNull(values); - - StringBuilder builder = new StringBuilder(); - for (DayOfWeek value: values) { - throwIfNull(value); - builder.append(value).append(","); - } - - addToPeriod(CronPeriod.DayOfWeek, builder.deleteCharAt(builder.length() - 1).toString()); - - return this; + return addInternal(CronPeriod.DayOfWeek, values); } - - public CronExpressionBuilder addRange(CronPeriod period, int from, int to) { - throwIfNull(period); - validateRange(from, to); - validatePeriod(period, from); - validatePeriod(period, to); - - addToPeriod(period, from + "-" + to); - return this; + /** + * Adds a range of values to a cron period. + * + * @param period The cron period to modify (e.g., {CronPeriod.MONTHOFYEAR}). + * @param from The starting value of the range (inclusive). + * @param to The ending value of the range (inclusive). + * @return The {@link CronExpressionBuilder} instance for method chaining. + * @throws IllegalArgumentException if the range is invalid. + */ + public CronExpressionBuilder addRange(CronPeriod period, int from, int to) { + return addRangeInternal(period, from, to); } + /** + * Adds a range of {@link DayOfWeek} values to the cron expression. + * + * @param from The starting {@link DayOfWeek} value. + * @param to The ending {@link DayOfWeek} value. + * @return The {@link CronExpressionBuilder} instance for method chaining. + */ public CronExpressionBuilder addRange(DayOfWeek from, DayOfWeek to) { - throwIfNull(from); - throwIfNull(to); - validateRange(from.getValue(), to.getValue()); - - addToPeriod(CronPeriod.DayOfWeek, from + "-" + to); - return this; + return addRangeInternal(CronPeriod.DayOfWeek, from, to); } + /** + * Adds a range of {@link MonthOfYear} values to the cron expression. + * + * @param from The starting {@link MonthOfYear} value. + * @param to The ending {@link MonthOfYear} value. + * @return The {@link CronExpressionBuilder} instance for method chaining. + */ public CronExpressionBuilder addRange(MonthOfYear from, MonthOfYear to) { - throwIfNull(from); - throwIfNull(to); - - addToPeriod(CronPeriod.MonthOfYear, from + "-" + to); - return this; + return addRangeInternal(CronPeriod.MonthOfYear, from, to); } + /** + * Adds a step range for a cron period. + * + * @param period The cron period to modify. + * @param from The starting value for the step range (inclusive). + * @param to The ending value for the step range (inclusive). + * @param interval The interval for the step range. + * @return The {@link CronExpressionBuilder} instance for method chaining. + */ public CronExpressionBuilder addStepRange(CronPeriod period, int from, int to, int interval) { - throwIfNull(period); - validateRange(from, to); - validatePeriod(period, interval); - - addToPeriod(period, from + "-" + to + "/" + interval); - return this; + return addStepInternal(period, from, to, interval); } - public CronExpressionBuilder addStep(CronPeriod period, int numerator, int interval) { - throwIfNull(period); - validatePeriod(period, numerator); - validatePeriod(period, interval); - - addToPeriod(period, numerator + "/" + interval); - return this; + /** + * Adds a step for a cron period. + * + * @param period The cron period to modify. + * @param interval The interval for the step. + * @return The {@link CronExpressionBuilder} instance for method chaining. + */ + public CronExpressionBuilder addStep(CronPeriod period, int interval) { + return addStepInternal(period, null, null, interval); } - public CronExpressionBuilder addStep(CronPeriod period, int interval) { + /** + * Adds a specific value with a step interval for the cron period. + * + * @param period The cron period to modify. + * @param value The starting value for the step. + * @param interval The interval for the step. + * @return The {@link CronExpressionBuilder} instance for method chaining. + */ + public CronExpressionBuilder addStep(CronPeriod period, int value, int interval) { throwIfNull(period); + validatePeriod(period, value); validatePeriod(period, interval); - addToPeriod(period, "*/" + interval); + addToPeriod(period, value + "/" + interval); return this; } + /** + * Builds the cron expression by combining all the specified cron periods and their values. + * + * @return A string representation of the cron expression. + */ public String build() { if (this.monthOfYear.isEmpty()) { @@ -152,17 +182,110 @@ public String build() { this.dayOfMonth.add("*"); } - StringBuilder cronExpression = new StringBuilder(); - cronExpression.append(String.join(",", this.seconds)).append(" "); - cronExpression.append(String.join(",", this.minutes)).append(" "); - cronExpression.append(String.join(",", this.hours)).append(" "); - cronExpression.append(String.join(",", this.dayOfMonth)).append(" "); - cronExpression.append(String.join(",", this.monthOfYear)).append(" "); - cronExpression.append(String.join(",", this.dayOfWeek)); + return String.join(",", this.seconds) + " " + + String.join(",", this.minutes) + " " + + String.join(",", this.hours) + " " + + String.join(",", this.dayOfMonth) + " " + + String.join(",", this.monthOfYear) + " " + + String.join(",", this.dayOfWeek); + } + + /** + * Internal method to add values to a cron period. + * + * @param period The cron period to modify. + * @param values The values to add. + * @return The {@link CronExpressionBuilder} instance for method chaining. + */ + private CronExpressionBuilder addInternal(CronPeriod period, T[] values) { + throwIfNull(period); + throwIfNull(values); + + if (values.length == 0) { + throw new IllegalArgumentException(period + " values cannot be empty"); + } + + List valueStrings = new ArrayList<>(values.length); + for (T value : values) { + throwIfNull(value); + if (value instanceof OrdinalEnum) { + validatePeriod(period, ((OrdinalEnum) value).getRank()); + valueStrings.add(value.toString()); + } else { + validatePeriod(period, (int)value); + valueStrings.add(String.valueOf(value)); + } + } + + addToPeriod(period, String.join(",", valueStrings)); + + return this; + } + + /** + * Internal method to add a range of values to a cron period. + * + * @param period The cron period to modify. + * @param from The starting value for the range (inclusive). + * @param to The ending value for the range (inclusive). + * @return The {@link CronExpressionBuilder} instance for method chaining. + */ + private CronExpressionBuilder addRangeInternal(CronPeriod period, T from, T to) { + throwIfNull(period); + throwIfNull(from); + throwIfNull(to); + + if (from instanceof OrdinalEnum && to instanceof OrdinalEnum) { + int fromInterval = ((OrdinalEnum) from).getRank(); + int toInterval = ((OrdinalEnum) to).getRank(); + validateRange(fromInterval, toInterval); + validatePeriod(period, fromInterval); + validatePeriod(period, toInterval); + } else { + validateRange((int)from, (int)to); + validatePeriod(period, (int)from); + validatePeriod(period, (int)to); + } + + addToPeriod(period, from + "-" + to); + return this; + } + + /** + * Internal method to add a step range to a cron period. + * + * @param period The cron period to modify. + * @param from The starting value for the step range (inclusive). + * @param to The ending value for the step range (inclusive). + * @param interval The interval for the step. + * @return The {@link CronExpressionBuilder} instance for method chaining. + */ + private CronExpressionBuilder addStepInternal(CronPeriod period, Integer from, Integer to, Integer interval) { + throwIfNull(period); + throwIfNull(interval); - return cronExpression.toString(); + if (from != null || to != null) { + throwIfNull(from); + throwIfNull(to); + validatePeriod(period, from); + validatePeriod(period, to); + validateRange(from, to); + + addToPeriod(period, from + "-" + to + "/" + interval); + return this; + } + + addToPeriod(period, "*/" + interval); + return this; } + /** + * Validates the value for a specific cron period. + * + * @param cronPeriod The cron period to validate (e.g., {@link CronPeriod.HOURS}). + * @param value The value to validate. + * @throws IllegalArgumentException if the value is invalid for the cron period. + */ private void validatePeriod(CronPeriod cronPeriod, int value) { switch (cronPeriod) { case SECONDS: @@ -201,6 +324,12 @@ private void validateRange(int from, int to) { } } + /** + * Helper method to add values to the period. + * + * @param period The cron period to modify. + * @param value The value to add to the period. + */ private void addToPeriod(CronPeriod cronPeriod, String value) { switch (cronPeriod) { case SECONDS: diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/CronPeriod.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/CronPeriod.java index b58b8a6454..04449f5ac8 100644 --- a/sdk-jobs/src/main/java/io/dapr/jobs/client/CronPeriod.java +++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/CronPeriod.java @@ -1,16 +1,46 @@ package io.dapr.jobs.client; +/** + * Represents the different fields of a cron expression that can be modified + * using the {@link CronExpressionBuilder}. + *

+ * Each enum value corresponds to a specific component of a cron schedule. + *

+ * Example usage: + *

+ * CronPeriod period = CronPeriod.MINUTES;
+ * System.out.println(period); // Outputs: MINUTES
+ * 
+ */ public enum CronPeriod { + /** + * Represents the seconds field in a cron expression (0-59). + */ SECONDS, + /** + * Represents the minutes field in a cron expression (0-59). + */ MINUTES, + /** + * Represents the hours field in a cron expression (0-23). + */ HOURS, + /** + * Represents the day of the month field in a cron expression (1-31). + */ DayOfMonth, + /** + * Represents the month of the year field in a cron expression (1-12). + */ MonthOfYear, + /** + * Represents the day of the week field in a cron expression (0-6, where 0 is Sunday). + */ DayOfWeek, -} +} \ No newline at end of file diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/DayOfWeek.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/DayOfWeek.java index ff3a1d705c..7a5c5999f0 100644 --- a/sdk-jobs/src/main/java/io/dapr/jobs/client/DayOfWeek.java +++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/DayOfWeek.java @@ -1,6 +1,21 @@ package io.dapr.jobs.client; -public enum DayOfWeek { +/** + * Represents the days of the week in a cron expression. + *

+ * This enum maps each day of the week to its corresponding integer value + * as used in cron expressions (0 for Sunday through 6 for Saturday). + *

+ * Implements {@link OrdinalEnum} to provide an ordinal ranking for each day. + *

+ * Example usage: + *

+ * DayOfWeek day = DayOfWeek.MON;
+ * System.out.println(day.getRank()); // Outputs: 1
+ * 
+ */ +public enum DayOfWeek implements OrdinalEnum { + SUN(0), MON(1), @@ -12,11 +27,17 @@ public enum DayOfWeek { private final int value; - private DayOfWeek(int value) { + /** + * Constructs a {@code DayOfWeek} enum with the given value. + * + * @param value the integer representation of the day (0-6). + */ + DayOfWeek(int value) { this.value = value; } - int getValue() { + @Override + public int getRank() { return this.value; } } \ No newline at end of file diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/JobsSchedule.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/JobsSchedule.java index b602b92886..da4f069c8c 100644 --- a/sdk-jobs/src/main/java/io/dapr/jobs/client/JobsSchedule.java +++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/JobsSchedule.java @@ -2,14 +2,47 @@ import java.time.Duration; +/** + * Represents a job schedule using cron expressions or fixed intervals. + *

+ * This class provides various static methods to create schedules based on predefined periods + * (e.g., daily, weekly, monthly) or using custom cron expressions. + *

+ * Example usage: + *

+ * JobsSchedule schedule = JobsSchedule.daily();
+ * System.out.println(schedule.getExpression()); // Outputs: "0 0 0 * * *"
+ * 
+ */ public class JobsSchedule { private final String expression; + /** + * Private constructor to create a job schedule from a cron expression. + * + * @param expression the cron expression defining the schedule. + */ private JobsSchedule(String expression) { this.expression = expression; } + /** + * Creates a job schedule from a fixed period using a {@link Duration}. + *

+ * The resulting expression follows the format: "@every XhYmZsWms" + * where X, Y, Z, and W represent hours, minutes, seconds, and milliseconds respectively. + *

+ * Example: + *

+   * JobsSchedule schedule = JobsSchedule.fromPeriod(Duration.ofMinutes(30));
+   * System.out.println(schedule.getExpression()); // Outputs: "@every 0h30m0s0ms"
+   * 
+ * + * @param duration the duration of the period. + * @return a {@code JobsSchedule} with the corresponding interval. + * @throws IllegalArgumentException if the duration is null. + */ public static JobsSchedule fromPeriod(Duration duration) { if (duration == null) { throw new IllegalArgumentException("duration cannot be null"); @@ -20,10 +53,21 @@ public static JobsSchedule fromPeriod(Duration duration) { return new JobsSchedule("@every " + formattedDuration); } + /** + * Creates a job schedule from a custom cron expression. + * + * @param cronExpression the cron expression. + * @return a {@code JobsSchedule} representing the given cron expression. + */ public static JobsSchedule fromString(String cronExpression) { return new JobsSchedule(cronExpression); } + /** + * Creates a yearly job schedule, running at midnight on January 1st. + * + * @return a {@code JobsSchedule} for yearly execution. + */ public static JobsSchedule yearly() { return new JobsSchedule(new CronExpressionBuilder() .add(CronPeriod.SECONDS, 0) @@ -34,6 +78,11 @@ public static JobsSchedule yearly() { .build()); } + /** + * Creates a monthly job schedule, running at midnight on the first day of each month. + * + * @return a {@code JobsSchedule} for monthly execution. + */ public static JobsSchedule monthly() { return new JobsSchedule(new CronExpressionBuilder() .add(CronPeriod.SECONDS, 0) @@ -43,6 +92,11 @@ public static JobsSchedule monthly() { .build()); } + /** + * Creates a weekly job schedule, running at midnight on Sunday. + * + * @return a {@code JobsSchedule} for weekly execution. + */ public static JobsSchedule weekly() { return new JobsSchedule(new CronExpressionBuilder() .add(CronPeriod.SECONDS, 0) @@ -52,6 +106,11 @@ public static JobsSchedule weekly() { .build()); } + /** + * Creates a daily job schedule, running at midnight every day. + * + * @return a {@code JobsSchedule} for daily execution. + */ public static JobsSchedule daily() { return new JobsSchedule(new CronExpressionBuilder() .add(CronPeriod.SECONDS, 0) @@ -60,6 +119,11 @@ public static JobsSchedule daily() { .build()); } + /** + * Creates an hourly job schedule, running at the start of every hour. + * + * @return a {@code JobsSchedule} for hourly execution. + */ public static JobsSchedule hourly() { return new JobsSchedule(new CronExpressionBuilder() .add(CronPeriod.SECONDS, 0) @@ -67,6 +131,11 @@ public static JobsSchedule hourly() { .build()); } + /** + * Gets the cron expression representing this job schedule. + * + * @return the cron expression as a string. + */ public String getExpression() { return this.expression; } diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/MonthOfYear.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/MonthOfYear.java index d9def02d68..2c47d7915e 100644 --- a/sdk-jobs/src/main/java/io/dapr/jobs/client/MonthOfYear.java +++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/MonthOfYear.java @@ -1,6 +1,18 @@ package io.dapr.jobs.client; -public enum MonthOfYear { +/** + * Represents the months of the year, each associated with its ordinal position (1-12). + *

+ * This enum implements {@link OrdinalEnum}, allowing retrieval of the numerical rank of each month. + *

+ * Example usage: + *

+ * MonthOfYear month = MonthOfYear.JAN;
+ * int rank = month.getRank(); // Returns 1
+ * 
+ */ + +public enum MonthOfYear implements OrdinalEnum { JAN(1), FEB(2), @@ -21,7 +33,8 @@ public enum MonthOfYear { this.value = value; } - int getValue() { + @Override + public int getRank() { return this.value; } } \ No newline at end of file diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/OrdinalEnum.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/OrdinalEnum.java new file mode 100644 index 0000000000..8ee171a2c9 --- /dev/null +++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/OrdinalEnum.java @@ -0,0 +1,11 @@ +package io.dapr.jobs.client; + +public interface OrdinalEnum { + + /** + * Returns the ordinal rank of the implementing enum. + * + * @return the rank as an integer. + */ + public int getRank(); +} diff --git a/sdk-jobs/test/test/io/dapr/jobs/CronExpressionBuilderTest.java b/sdk-jobs/test/test/io/dapr/jobs/CronExpressionBuilderTest.java index 3ee235d9c0..5bfcd65334 100644 --- a/sdk-jobs/test/test/io/dapr/jobs/CronExpressionBuilderTest.java +++ b/sdk-jobs/test/test/io/dapr/jobs/CronExpressionBuilderTest.java @@ -4,6 +4,7 @@ import io.dapr.jobs.client.CronPeriod; import io.dapr.jobs.client.DayOfWeek; import io.dapr.jobs.client.MonthOfYear; +import org.checkerframework.checker.units.qual.C; import org.junit.Assert; import org.junit.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -43,6 +44,14 @@ public void builderWithInvalidMinutesShouldThrowIllegalArgumentException() { Assert.assertTrue(exception.getMessage().contains("MINUTES must be between [0, 59]")); } + @Test + public void builderWithEmptyParametersShouldThrowIllegalArgumentException() { + IllegalArgumentException exception = Assert.assertThrows(IllegalArgumentException.class, + () -> new CronExpressionBuilder() + .add(CronPeriod.MINUTES).build()); + Assert.assertTrue(exception.getMessage().contains("MINUTES values cannot be empty")); + } + @Test public void builderWithInvalidHoursShouldThrowIllegalArgumentException() { IllegalArgumentException exception = Assert.assertThrows(IllegalArgumentException.class, @@ -322,4 +331,18 @@ public void builderWithCallToAddForStepShouldReturnCorrectValues() { .build(); assertEquals("* * 20/2 * JAN,FEB MON,THU", cronExpression); } + + @Test + public void builderWithCallToAddAllFieldsShouldReturnCorrectValues() { + String cronExpression = new CronExpressionBuilder() + .add(MonthOfYear.JAN, MonthOfYear.FEB) + .add(DayOfWeek.MON, DayOfWeek.THU) + .addStep(CronPeriod.HOURS, 20, 2) + .add(CronPeriod.SECONDS, 1) + .add(CronPeriod.MINUTES, 1) + .add(CronPeriod.HOURS, 1) + .add(CronPeriod.DayOfMonth, 1) + .build(); + assertEquals("1 1 20/2,1 1 JAN,FEB MON,THU", cronExpression); + } } From 1e072e06828534ee383661519a5a32b9fe00e9bb Mon Sep 17 00:00:00 2001 From: sirivarma Date: Sun, 9 Mar 2025 13:17:02 -0700 Subject: [PATCH 4/4] Add things --- .../io/dapr/jobs/client/CronExpressionBuilder.java | 13 ++++++------- .../main/java/io/dapr/jobs/client/CronPeriod.java | 2 -- .../main/java/io/dapr/jobs/client/DayOfWeek.java | 3 --- .../main/java/io/dapr/jobs/client/JobsSchedule.java | 4 ---- .../main/java/io/dapr/jobs/client/MonthOfYear.java | 2 -- .../main/java/io/dapr/jobs/client/OrdinalEnum.java | 7 ++++++- 6 files changed, 12 insertions(+), 19 deletions(-) diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/CronExpressionBuilder.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/CronExpressionBuilder.java index d8c633186a..f2111bb400 100644 --- a/sdk-jobs/src/main/java/io/dapr/jobs/client/CronExpressionBuilder.java +++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/CronExpressionBuilder.java @@ -8,7 +8,6 @@ * A builder class for constructing cron expressions. This class provides an easy way to construct cron expressions * by adding individual values or ranges for each of the cron fields: seconds, minutes, hours, day of month, * day of week, and month of year. It supports adding steps and ranges for fields where appropriate. - *

* Example usage: *

  * CronExpressionBuilder builder = new CronExpressionBuilder();
@@ -182,12 +181,12 @@ public String build() {
       this.dayOfMonth.add("*");
     }
 
-    return String.join(",", this.seconds) + " " +
-        String.join(",", this.minutes) + " " +
-        String.join(",", this.hours) + " " +
-        String.join(",", this.dayOfMonth) + " " +
-        String.join(",", this.monthOfYear) + " " +
-        String.join(",", this.dayOfWeek);
+    return String.join(",", this.seconds) + " "
+        + String.join(",", this.minutes) + " "
+        + String.join(",", this.hours) + " "
+        + String.join(",", this.dayOfMonth) + " "
+        + String.join(",", this.monthOfYear) + " "
+        + String.join(",", this.dayOfWeek);
   }
 
   /**
diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/CronPeriod.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/CronPeriod.java
index 04449f5ac8..f8172d105f 100644
--- a/sdk-jobs/src/main/java/io/dapr/jobs/client/CronPeriod.java
+++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/CronPeriod.java
@@ -3,9 +3,7 @@
 /**
  * Represents the different fields of a cron expression that can be modified
  * using the {@link CronExpressionBuilder}.
- * 

* Each enum value corresponds to a specific component of a cron schedule. - *

* Example usage: *

  * CronPeriod period = CronPeriod.MINUTES;
diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/DayOfWeek.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/DayOfWeek.java
index 7a5c5999f0..63f6f216ed 100644
--- a/sdk-jobs/src/main/java/io/dapr/jobs/client/DayOfWeek.java
+++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/DayOfWeek.java
@@ -2,12 +2,9 @@
 
 /**
  * Represents the days of the week in a cron expression.
- * 

* This enum maps each day of the week to its corresponding integer value * as used in cron expressions (0 for Sunday through 6 for Saturday). - *

* Implements {@link OrdinalEnum} to provide an ordinal ranking for each day. - *

* Example usage: *

  * DayOfWeek day = DayOfWeek.MON;
diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/JobsSchedule.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/JobsSchedule.java
index da4f069c8c..a4192ea732 100644
--- a/sdk-jobs/src/main/java/io/dapr/jobs/client/JobsSchedule.java
+++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/JobsSchedule.java
@@ -4,10 +4,8 @@
 
 /**
  * Represents a job schedule using cron expressions or fixed intervals.
- * 

* This class provides various static methods to create schedules based on predefined periods * (e.g., daily, weekly, monthly) or using custom cron expressions. - *

* Example usage: *

  * JobsSchedule schedule = JobsSchedule.daily();
@@ -29,10 +27,8 @@ private JobsSchedule(String expression) {
 
   /**
    * Creates a job schedule from a fixed period using a {@link Duration}.
-   * 

* The resulting expression follows the format: "@every XhYmZsWms" * where X, Y, Z, and W represent hours, minutes, seconds, and milliseconds respectively. - *

* Example: *

    * JobsSchedule schedule = JobsSchedule.fromPeriod(Duration.ofMinutes(30));
diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/MonthOfYear.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/MonthOfYear.java
index 2c47d7915e..57fc4ed36c 100644
--- a/sdk-jobs/src/main/java/io/dapr/jobs/client/MonthOfYear.java
+++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/MonthOfYear.java
@@ -2,9 +2,7 @@
 
 /**
  * Represents the months of the year, each associated with its ordinal position (1-12).
- * 

* This enum implements {@link OrdinalEnum}, allowing retrieval of the numerical rank of each month. - *

* Example usage: *

  * MonthOfYear month = MonthOfYear.JAN;
diff --git a/sdk-jobs/src/main/java/io/dapr/jobs/client/OrdinalEnum.java b/sdk-jobs/src/main/java/io/dapr/jobs/client/OrdinalEnum.java
index 8ee171a2c9..d86ed7545f 100644
--- a/sdk-jobs/src/main/java/io/dapr/jobs/client/OrdinalEnum.java
+++ b/sdk-jobs/src/main/java/io/dapr/jobs/client/OrdinalEnum.java
@@ -1,5 +1,10 @@
 package io.dapr.jobs.client;
 
+/**
+ * Represents an enumeration that has an associated ordinal rank.
+ * This interface is intended to be implemented by enums that need to provide
+ * a numerical representation for their values, such as days of the week or months of the year.
+ */
 public interface OrdinalEnum {
 
   /**
@@ -7,5 +12,5 @@ public interface OrdinalEnum {
    *
    * @return the rank as an integer.
    */
-  public int getRank();
+  int getRank();
 }