Skip to content

Commit

Permalink
Scheduler: introduce a shared CronParser util class
Browse files Browse the repository at this point in the history
- fix conversion from spring CRON format
  • Loading branch information
mkouba committed Sep 25, 2024
1 parent d976547 commit 4a97580
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String, QuartzTrigger> scheduledTasks = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -258,7 +249,7 @@ public org.quartz.Trigger apply(TriggerKey triggerKey) {
SchedulerUtils.parseExecutionMaxDelayAsMillis(scheduled), blockingExecutor);

JobDetail jobDetail = createJobDetail(identity, method.getInvokerClassName());
Optional<TriggerBuilder<?>> triggerBuilder = createTrigger(identity, scheduled, cronType, runtimeConfig,
Optional<TriggerBuilder<?>> triggerBuilder = createTrigger(identity, scheduled, runtimeConfig,
jobDetail);

if (triggerBuilder.isPresent()) {
Expand Down Expand Up @@ -711,29 +702,16 @@ private JobDetail createJobDetail(String identity, String invokerClassName) {
* @return the trigger builder
* @see SchedulerUtils#isOff(String)
*/
private Optional<TriggerBuilder<?>> createTrigger(String identity, Scheduled scheduled, CronType cronType,
private Optional<TriggerBuilder<?>> 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) {
Expand Down Expand Up @@ -1005,7 +983,7 @@ public boolean isBlocking() {
JobDetail jobDetail = jobBuilder.requestRecovery().build();

org.quartz.Trigger trigger;
Optional<TriggerBuilder<?>> triggerBuilder = createTrigger(scheduled.identity(), scheduled, cronType, runtimeConfig,
Optional<TriggerBuilder<?>> triggerBuilder = createTrigger(scheduled.identity(), scheduled, runtimeConfig,
jobDetail);
if (triggerBuilder.isPresent()) {
if (oldTrigger != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Cron, Cron> 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<CronFieldName, CronField> 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()));
};
}

}
Original file line number Diff line number Diff line change
@@ -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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -182,7 +179,7 @@ public void run() {
if (id.isEmpty()) {
id = nameSequence + "_" + method.getMethodDescription();
}
Optional<SimpleTrigger> trigger = createTrigger(id, method.getMethodDescription(), cronParser, scheduled,
Optional<SimpleTrigger> trigger = createTrigger(id, method.getMethodDescription(), scheduled,
defaultOverdueGracePeriod);
if (trigger.isPresent()) {
JobInstrumenter instrumenter = null;
Expand Down Expand Up @@ -352,7 +349,7 @@ public Trigger getScheduledJob(String identity) {
return null;
}

Optional<SimpleTrigger> createTrigger(String id, String methodDescription, CronParser parser, Scheduled scheduled,
Optional<SimpleTrigger> createTrigger(String id, String methodDescription, Scheduled scheduled,
Duration defaultGracePeriod) {
ZonedDateTime start = ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS);
Long millisToAdd = null;
Expand All @@ -365,18 +362,12 @@ Optional<SimpleTrigger> 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()) {
Expand Down Expand Up @@ -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<SimpleTrigger> trigger = createTrigger(identity, null, cronParser, scheduled,
defaultOverdueGracePeriod);
Optional<SimpleTrigger> trigger = createTrigger(identity, null, scheduled, defaultOverdueGracePeriod);
if (trigger.isPresent()) {
SimpleTrigger simpleTrigger = trigger.get();
JobInstrumenter instrumenter = null;
Expand Down

0 comments on commit 4a97580

Please sign in to comment.