Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#57): Add Trigger rule #74

Merged
merged 2 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ object JacksonLibs {

const val annotations = "com.fasterxml.jackson.core:jackson-annotations:${Version.jackson}"
const val databind = "com.fasterxml.jackson.core:jackson-databind:${Version.jackson}"
const val jsr310 = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${Version.jackson}"
}

object TestLibs {
Expand Down
1 change: 1 addition & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies {

testImplementation(TestLibs.junit5Params)
testImplementation(JacksonLibs.databind)
testImplementation(JacksonLibs.jsr310)
testImplementation(LogLibs.logback)
testCompileOnly(UtilLibs.jetbrainsAnnotations)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,11 @@ protected final TriggerContext shouldStop(@NotNull TriggerContext triggerContext
}

protected TriggerContext evaluateTrigger(@NotNull TriggerContext triggerContext) {
return trigger().shouldExecute(Objects.requireNonNull(triggerContext.triggerAt()))
final Instant triggerAt = Objects.requireNonNull(triggerContext.triggerAt());
if (trigger().rule().isExceeded(triggerAt)) {
return TriggerContextFactory.stop(triggerContext, ReasonCode.STOP_BY_CONFIG);
}
return trigger().shouldExecute(triggerAt)
? TriggerContextFactory.ready(triggerContext)
: TriggerContextFactory.skip(triggerContext, ReasonCode.CONDITION_IS_NOT_MATCHED);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ public static ChronoUnit toChronoUnit(TimeUnit timeUnit) {
}

@SuppressWarnings("unchecked")
public static <T> T castOrNull(Object data, boolean nullIfThrow) {
public static <T> T castOrNull(Object data, boolean nullOrThrow) {
try {
return (T) data;
} catch (ClassCastException ex) {
// @formatter:off
if (nullIfThrow) return null;
if (nullOrThrow) return null;
throw ex;
// @formatter:on
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

import org.jetbrains.annotations.NotNull;

import io.github.zero88.schedulerx.trigger.rule.TriggerRule;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

Expand Down Expand Up @@ -85,12 +87,18 @@ public long nextTriggerAfter(@NotNull Instant current) {
@Override
public @NotNull List<OffsetDateTime> preview(@NotNull PreviewParameter parameter) {
validate();
Instant next = parameter.getStartedAt();
final ZoneId zoneId = Optional.ofNullable(parameter.getTimeZone()).orElseGet(timeZone::toZoneId);
final List<OffsetDateTime> result = new ArrayList<>();
final TriggerRule rule = parameter.getRule();
final ZoneId zoneId = Optional.ofNullable(parameter.getTimeZone()).orElseGet(timeZone::toZoneId);
Instant next = parameter.getStartedAt();
do {
next = cronExpression.getNextValidTimeAfter(Date.from(next)).toInstant();
result.add(next.atZone(zoneId).toOffsetDateTime());
if (rule.isExceeded(next)) {
break;
}
if (rule.satisfy(next)) {
result.add(next.atZone(zoneId).toOffsetDateTime());
}
} while (result.size() != parameter.getTimes());
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.jetbrains.annotations.NotNull;

import io.github.zero88.schedulerx.impl.Utils;
import io.github.zero88.schedulerx.trigger.rule.TriggerRule;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

Expand Down Expand Up @@ -42,7 +43,7 @@ public final class IntervalTrigger implements Trigger {
*/
private final long repeat;
/**
* Get the initial delay time (in {@link #getInitialDelayTimeUnit()}) before firing trigger in first time.
* Get the initial delay time (in {@link #getInitialDelayTimeUnit()}) before emitting trigger in the first time.
*
* @apiNote Default is {@code 0}
*/
Expand Down Expand Up @@ -116,20 +117,26 @@ public boolean shouldStop(long round) {

@Override
public @NotNull List<OffsetDateTime> preview(@NotNull PreviewParameter parameter) {
final long count = Math.min(repeat, parameter.getTimes());
final List<OffsetDateTime> result = new ArrayList<>();
final long count = Math.min(repeat, parameter.getTimes());
final TriggerRule rule = parameter.getRule();
final ZoneId zoneId = Optional.ofNullable(parameter.getTimeZone()).orElse(ZoneOffset.UTC);
Instant next = parameter.getStartedAt();
next = next.plus(initialDelay, Utils.toChronoUnit(initialDelayTimeUnit));
do {
next = next.plus(interval, Utils.toChronoUnit(intervalTimeUnit));
result.add(next.atZone(zoneId).toOffsetDateTime());
if (rule.isExceeded(next)) {
break;
}
if (rule.satisfy(next)) {
result.add(next.atZone(zoneId).toOffsetDateTime());
}
} while (result.size() != count);
return result;
}

static void validate(long number, boolean allowZero, boolean allowIndefinitely, String msg) {
if (number > 0 || (allowZero && number == 0) || (allowIndefinitely && number == REPEAT_INDEFINITELY)) {
static void validate(long number, boolean allowZero, boolean allowInfinite, String msg) {
if (number > 0 || (allowZero && number == 0) || (allowInfinite && number == REPEAT_INDEFINITELY)) {
return;
}
throw new IllegalArgumentException("Invalid " + msg + " value");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Objects;
import java.util.Optional;
import java.util.TimeZone;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import io.github.zero88.schedulerx.trigger.rule.TriggerRule;

/**
* The preview parameter to generate the next trigger time of the specific trigger.
*
Expand All @@ -19,9 +20,10 @@ public final class PreviewParameter {

public static final int MAX_TIMES = 30;

private Instant startedAt;
private Instant startedAt = Instant.now();
private int times;
private ZoneId timeZone;
private TriggerRule rule = TriggerRule.NOOP;

/**
* Create default the preview parameter with startedAt is now and times = 10
Expand All @@ -35,8 +37,8 @@ public static PreviewParameter byDefault() {
/**
* @return the started at time to generate the preview results
*/
public Instant getStartedAt() {
return Optional.ofNullable(startedAt).orElseGet(Instant::now);
public @NotNull Instant getStartedAt() {
return startedAt;
}

/**
Expand All @@ -45,8 +47,10 @@ public Instant getStartedAt() {
* @param startedAt started at time
* @return this for fluent API
*/
public PreviewParameter setStartedAt(Instant startedAt) {
this.startedAt = startedAt;
public @NotNull PreviewParameter setStartedAt(Instant startedAt) {
if (startedAt != null) {
this.startedAt = startedAt;
}
return this;
}

Expand All @@ -63,7 +67,7 @@ public int getTimes() {
* @param times the number of a preview item
* @return this for fluent API
*/
public PreviewParameter setTimes(int times) {
public @NotNull PreviewParameter setTimes(int times) {
this.times = times;
return this;
}
Expand Down Expand Up @@ -108,4 +112,25 @@ public PreviewParameter setTimeZone(ZoneOffset zoneOffset) {
return this;
}

/**
* @return the trigger rule
* @see TriggerRule
*/
public @NotNull TriggerRule getRule() {
return rule;
}

/**
* Set a trigger rule in the preview parameter
*
* @param rule trigger rule
* @return this for fluent API
*/
public @NotNull PreviewParameter setRule(TriggerRule rule) {
if (rule != null) {
this.rule = rule;
}
return this;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
import io.github.zero88.schedulerx.Task;
import io.github.zero88.schedulerx.spi.TriggerRepresentation;
import io.github.zero88.schedulerx.spi.TriggerRepresentationServiceLoader;
import io.github.zero88.schedulerx.trigger.rule.TriggerRule;

/**
* Represents for inspecting settings specific to a Trigger, which is used to fire a <code>{@link Task}</code> at given
* moments in time
* moments in time.
*
* @since 1.0.0
*/
Expand All @@ -28,6 +29,17 @@ public interface Trigger extends HasTriggerType, TriggerRepresentation {
*/
@NotNull Trigger validate();

/**
* Defines the trigger rule
*
* @return the trigger rule
* @see TriggerRule
* @since 2.0.0
*/
default @NotNull TriggerRule rule() {
return TriggerRule.NOOP;
}

/**
* Verify if the trigger time still appropriate to execute the task.
* <p/>
Expand All @@ -36,7 +48,7 @@ public interface Trigger extends HasTriggerType, TriggerRepresentation {
* @param triggerAt the trigger time
* @since 2.0.0
*/
default boolean shouldExecute(@NotNull Instant triggerAt) { return true; }
default boolean shouldExecute(@NotNull Instant triggerAt) { return rule().satisfy(triggerAt); }

/**
* Verify the execution should be stopped after the current execution round is out of the trigger rule.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.github.zero88.schedulerx.trigger.rule;

import java.util.Objects;

import org.jetbrains.annotations.NotNull;

abstract class BaseTimeframe<T> implements Timeframe<T>, TimeParser<T> {

private T from;
private T to;

protected BaseTimeframe() { }

protected final BaseTimeframe<T> setValues(Object from, Object to) {
final TimeframeValidator validator = validator();
final TimeParser<T> parser = parser();
this.from = validator.normalize(parser, from);
this.to = validator.normalize(parser, to);
//noinspection unchecked
return (BaseTimeframe<T>) validator.validate(this);
}

@Override
public final T from() { return from; }

@Override
public final T to() { return to; }

@Override
public final @NotNull TimeParser<T> parser() { return this; }

@Override
public @NotNull Timeframe<T> set(@NotNull String field, Object value) {
throw new UnsupportedOperationException();
}

@Override
public int hashCode() {
int result = from.hashCode();
result = 31 * result + to.hashCode();
result = 31 * result + type().hashCode();
return result;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Timeframe<?> that = (Timeframe<?>) o;
return Objects.equals(type(), that.type()) && Objects.equals(from, that.from()) &&
Objects.equals(to, that.to());
}

@Override
public String toString() {
return "TimeFrame{type=" + type().getName() + ", (" + from + ", " + to + ")}";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.github.zero88.schedulerx.trigger.rule;

import java.time.Instant;

import org.jetbrains.annotations.NotNull;

public class InstantTimeframe extends BaseTimeframe<Instant> implements TimeRangeConstraint {

@Override
public final @NotNull Class<Instant> type() { return Instant.class; }

@Override
public boolean check(@NotNull Instant instant) {
return (from() == null || instant.isAfter(from())) && (to() == null || instant.isBefore(to()));
}

@Override
public Instant parse(Object value) {
if (value instanceof CharSequence) {
return Instant.parse((CharSequence) value);
}
return (Instant) value;
}

@Override
public @NotNull TimeframeValidator validator() {
return super.validator().and(this);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.github.zero88.schedulerx.trigger.rule;

import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;

import org.jetbrains.annotations.NotNull;

public class LocalDateTimeFrame extends BaseTimeframe<LocalDate> implements TimeRangeConstraint {

@Override
public final @NotNull Class<LocalDate> type() { return LocalDate.class; }

@Override
public boolean check(@NotNull Instant instant) {
final LocalDate given = instant.atZone(ZoneId.systemDefault()).toLocalDate();
return (from() == null || given.isAfter(from())) && (to() == null || given.isBefore(to()));
}

@Override
public LocalDate parse(Object value) {
if (value instanceof CharSequence) {
return LocalDate.parse((CharSequence) value);
}
return (LocalDate) value;
}

@Override
public @NotNull TimeframeValidator validator() {
return super.validator().and(this);
}

}
Loading
Loading