diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java index fb915d0322ff6..23b648d3825c3 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java @@ -59,13 +59,6 @@ import org.quartz.simpl.SimpleJobFactory; import org.quartz.spi.TriggerFiredBundle; -import com.cronutils.mapper.CronMapper; -import com.cronutils.model.Cron; -import com.cronutils.model.CronType; -import com.cronutils.model.definition.CronDefinition; -import com.cronutils.model.definition.CronDefinitionBuilder; -import com.cronutils.parser.CronParser; - import io.quarkus.arc.Subclass; import io.quarkus.quartz.QuartzScheduler; import io.quarkus.runtime.StartupEvent; @@ -83,6 +76,7 @@ import io.quarkus.scheduler.SuccessfulExecution; import io.quarkus.scheduler.Trigger; import io.quarkus.scheduler.common.runtime.AbstractJobDefinition; +import io.quarkus.scheduler.common.runtime.CronParser; import io.quarkus.scheduler.common.runtime.DefaultInvoker; import io.quarkus.scheduler.common.runtime.Events; import io.quarkus.scheduler.common.runtime.ScheduledInvoker; @@ -115,7 +109,6 @@ public class QuartzSchedulerImpl implements QuartzScheduler { private final boolean startHalted; private final Duration shutdownWaitTime; private final boolean enabled; - private final CronType cronType; private final CronParser cronParser; private final Duration defaultOverdueGracePeriod; private final Map scheduledTasks = new ConcurrentHashMap<>(); @@ -189,9 +182,7 @@ public QuartzSchedulerImpl(SchedulerContext context, QuartzSupport quartzSupport .collect(Collectors.joining(", "))); } - cronType = context.getCronType(); - CronDefinition def = CronDefinitionBuilder.instanceDefinitionFor(cronType); - cronParser = new CronParser(def); + cronParser = new CronParser(context.getCronType()); JobInstrumenter instrumenter = null; if (schedulerConfig.tracingEnabled && jobInstrumenter.isResolvable()) { @@ -258,7 +249,7 @@ public org.quartz.Trigger apply(TriggerKey triggerKey) { SchedulerUtils.parseExecutionMaxDelayAsMillis(scheduled), blockingExecutor); JobDetail jobDetail = createJobDetail(identity, method.getInvokerClassName()); - Optional> triggerBuilder = createTrigger(identity, scheduled, cronType, runtimeConfig, + Optional> triggerBuilder = createTrigger(identity, scheduled, runtimeConfig, jobDetail); if (triggerBuilder.isPresent()) { @@ -711,29 +702,16 @@ private JobDetail createJobDetail(String identity, String invokerClassName) { * @return the trigger builder * @see SchedulerUtils#isOff(String) */ - private Optional> createTrigger(String identity, Scheduled scheduled, CronType cronType, + private Optional> createTrigger(String identity, Scheduled scheduled, QuartzRuntimeConfig runtimeConfig, JobDetail jobDetail) { ScheduleBuilder scheduleBuilder; - String cron = SchedulerUtils.lookUpPropertyValue(scheduled.cron()); - if (!cron.isEmpty()) { + if (!scheduled.cron().isEmpty()) { + String cron = SchedulerUtils.lookUpPropertyValue(scheduled.cron()); if (SchedulerUtils.isOff(cron)) { return Optional.empty(); } - if (!CronType.QUARTZ.equals(cronType)) { - // Migrate the expression - Cron cronExpr = cronParser.parse(cron); - switch (cronType) { - case UNIX: - cron = CronMapper.fromUnixToQuartz().map(cronExpr).asString(); - break; - case CRON4J: - cron = CronMapper.fromCron4jToQuartz().map(cronExpr).asString(); - break; - default: - break; - } - } + cron = cronParser.mapToQuartz(cronParser.parse(cron)).asString(); CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cron); ZoneId timeZone = SchedulerUtils.parseCronTimeZone(scheduled); if (timeZone != null) { @@ -1005,7 +983,7 @@ public boolean isBlocking() { JobDetail jobDetail = jobBuilder.requestRecovery().build(); org.quartz.Trigger trigger; - Optional> triggerBuilder = createTrigger(scheduled.identity(), scheduled, cronType, runtimeConfig, + Optional> triggerBuilder = createTrigger(scheduled.identity(), scheduled, runtimeConfig, jobDetail); if (triggerBuilder.isPresent()) { if (oldTrigger != null) { diff --git a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/CronParser.java b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/CronParser.java new file mode 100644 index 0000000000000..f293572acfd13 --- /dev/null +++ b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/CronParser.java @@ -0,0 +1,90 @@ +package io.quarkus.scheduler.common.runtime; + +import static com.cronutils.model.field.expression.FieldExpression.questionMark; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.Map; + +import com.cronutils.Function; +import com.cronutils.mapper.CronMapper; +import com.cronutils.model.Cron; +import com.cronutils.model.CronType; +import com.cronutils.model.SingleCron; +import com.cronutils.model.definition.CronDefinitionBuilder; +import com.cronutils.model.field.CronField; +import com.cronutils.model.field.CronFieldName; +import com.cronutils.model.field.expression.Always; +import com.cronutils.model.field.expression.QuestionMark; + +public class CronParser { + + private final CronType cronType; + + private final com.cronutils.parser.CronParser cronParser; + + public CronParser(CronType cronType) { + this.cronType = cronType; + this.cronParser = new com.cronutils.parser.CronParser(CronDefinitionBuilder.instanceDefinitionFor(cronType)); + } + + public CronType cronType() { + return cronType; + } + + public Cron parse(String value) { + return cronParser.parse(value); + } + + public Cron mapToQuartz(Cron cron) { + switch (cronType) { + case QUARTZ: + return cron; + case UNIX: + return CronMapper.fromUnixToQuartz().map(cron); + case CRON4J: + return CronMapper.fromCron4jToQuartz().map(cron); + case SPRING: + return CronMapper.fromSpringToQuartz().map(cron); + case SPRING53: + // https://github.com/jmrozanec/cron-utils/issues/579 + return new CronMapper( + CronDefinitionBuilder.instanceDefinitionFor(CronType.SPRING53), + CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ), + setQuestionMark()).map(cron); + default: + throw new IllegalStateException("Unsupported cron type: " + cronType); + } + } + + // Copy from com.cronutils.mapper.CronMapper#setQuestionMark() + private static Function setQuestionMark() { + return cron -> { + final CronField dow = cron.retrieve(CronFieldName.DAY_OF_WEEK); + final CronField dom = cron.retrieve(CronFieldName.DAY_OF_MONTH); + if (dow == null && dom == null) { + return cron; + } + if (dow.getExpression() instanceof QuestionMark || dom.getExpression() instanceof QuestionMark) { + return cron; + } + final Map fields = new EnumMap<>(CronFieldName.class); + fields.putAll(cron.retrieveFieldsAsMap()); + if (dow.getExpression() instanceof Always) { + fields.put(CronFieldName.DAY_OF_WEEK, + new CronField(CronFieldName.DAY_OF_WEEK, questionMark(), + fields.get(CronFieldName.DAY_OF_WEEK).getConstraints())); + } else { + if (dom.getExpression() instanceof Always) { + fields.put(CronFieldName.DAY_OF_MONTH, + new CronField(CronFieldName.DAY_OF_MONTH, questionMark(), + fields.get(CronFieldName.DAY_OF_MONTH).getConstraints())); + } else { + cron.validate(); + } + } + return new SingleCron(cron.getCronDefinition(), new ArrayList<>(fields.values())); + }; + } + +} diff --git a/extensions/scheduler/common/src/test/java/io/quarkus/scheduler/common/runtime/CronParserTest.java b/extensions/scheduler/common/src/test/java/io/quarkus/scheduler/common/runtime/CronParserTest.java new file mode 100644 index 0000000000000..b298202af6c0c --- /dev/null +++ b/extensions/scheduler/common/src/test/java/io/quarkus/scheduler/common/runtime/CronParserTest.java @@ -0,0 +1,52 @@ +package io.quarkus.scheduler.common.runtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.junit.jupiter.api.Test; + +import com.cronutils.model.Cron; +import com.cronutils.model.CronType; + +public class CronParserTest { + + @Test + public void testMapUnixToQuartz() { + CronParser parser = new CronParser(CronType.UNIX); + Cron cron = parser.parse("10 14 * * 1"); + Cron quartz = parser.mapToQuartz(cron); + assertEquals("0 10 14 ? * 2 *", quartz.asString()); + } + + @Test + public void testMapQuartzToQuartz() { + CronParser parser = new CronParser(CronType.QUARTZ); + Cron cron = parser.parse("0 10 14 ? * 2 *"); + assertSame(cron, parser.mapToQuartz(cron)); + } + + @Test + public void testMapCron4jToQuartz() { + CronParser parser = new CronParser(CronType.CRON4J); + Cron cron = parser.parse("10 14 L * *"); + Cron quartz = parser.mapToQuartz(cron); + assertEquals("0 10 14 L * ? *", quartz.asString()); + } + + @Test + public void testMapSpringToQuartz() { + CronParser parser = new CronParser(CronType.SPRING); + Cron cron = parser.parse("1 10 14 * * 0"); + Cron quartz = parser.mapToQuartz(cron); + assertEquals("1 10 14 ? * 1 *", quartz.asString()); + } + + @Test + public void testMapSpring53ToQuartz() { + CronParser parser = new CronParser(CronType.SPRING53); + Cron cron = parser.parse("1 10 14 L * *"); + Cron quartz = parser.mapToQuartz(cron); + assertEquals("1 10 14 L * ? *", quartz.asString()); + } + +} diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java index 752cd332ec7f6..a9a4f9558fa05 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java @@ -33,10 +33,7 @@ import org.jboss.threads.JBossScheduledThreadPoolExecutor; import com.cronutils.model.Cron; -import com.cronutils.model.definition.CronDefinition; -import com.cronutils.model.definition.CronDefinitionBuilder; import com.cronutils.model.time.ExecutionTime; -import com.cronutils.parser.CronParser; import io.quarkus.runtime.StartupEvent; import io.quarkus.scheduler.DelayedExecution; @@ -54,6 +51,7 @@ import io.quarkus.scheduler.SuccessfulExecution; import io.quarkus.scheduler.Trigger; import io.quarkus.scheduler.common.runtime.AbstractJobDefinition; +import io.quarkus.scheduler.common.runtime.CronParser; import io.quarkus.scheduler.common.runtime.DefaultInvoker; import io.quarkus.scheduler.common.runtime.DelayedExecutionInvoker; import io.quarkus.scheduler.common.runtime.Events; @@ -122,8 +120,7 @@ public SimpleScheduler(SchedulerContext context, SchedulerRuntimeConfig schedule this.jobInstrumenter = jobInstrumenter; this.blockingExecutor = blockingExecutor; - CronDefinition definition = CronDefinitionBuilder.instanceDefinitionFor(context.getCronType()); - this.cronParser = new CronParser(definition); + this.cronParser = new CronParser(context.getCronType()); this.defaultOverdueGracePeriod = schedulerRuntimeConfig.overdueGracePeriod; if (!schedulerRuntimeConfig.enabled) { @@ -182,7 +179,7 @@ public void run() { if (id.isEmpty()) { id = nameSequence + "_" + method.getMethodDescription(); } - Optional trigger = createTrigger(id, method.getMethodDescription(), cronParser, scheduled, + Optional trigger = createTrigger(id, method.getMethodDescription(), scheduled, defaultOverdueGracePeriod); if (trigger.isPresent()) { JobInstrumenter instrumenter = null; @@ -352,7 +349,7 @@ public Trigger getScheduledJob(String identity) { return null; } - Optional createTrigger(String id, String methodDescription, CronParser parser, Scheduled scheduled, + Optional createTrigger(String id, String methodDescription, Scheduled scheduled, Duration defaultGracePeriod) { ZonedDateTime start = ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS); Long millisToAdd = null; @@ -365,18 +362,12 @@ Optional createTrigger(String id, String methodDescription, CronP start = start.toInstant().plusMillis(millisToAdd).atZone(start.getZone()); } - String cron = SchedulerUtils.lookUpPropertyValue(scheduled.cron()); - if (!cron.isEmpty()) { + if (!scheduled.cron().isEmpty()) { + String cron = SchedulerUtils.lookUpPropertyValue(scheduled.cron()); if (SchedulerUtils.isOff(cron)) { return Optional.empty(); } - Cron cronExpr; - try { - cronExpr = parser.parse(cron); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Cannot parse cron expression: " + cron, e); - } - return Optional.of(new CronTrigger(id, start, cronExpr, + return Optional.of(new CronTrigger(id, start, cronParser.parse(cron), SchedulerUtils.parseOverdueGracePeriod(scheduled, defaultGracePeriod), SchedulerUtils.parseCronTimeZone(scheduled), methodDescription)); } else if (!scheduled.every().isEmpty()) { @@ -708,8 +699,7 @@ public boolean isBlocking() { } Scheduled scheduled = new SyntheticScheduled(identity, cron, every, 0, TimeUnit.MINUTES, delayed, overdueGracePeriod, concurrentExecution, skipPredicate, timeZone, implementation, executionMaxDelay); - Optional trigger = createTrigger(identity, null, cronParser, scheduled, - defaultOverdueGracePeriod); + Optional trigger = createTrigger(identity, null, scheduled, defaultOverdueGracePeriod); if (trigger.isPresent()) { SimpleTrigger simpleTrigger = trigger.get(); JobInstrumenter instrumenter = null;