Skip to content

Commit

Permalink
#1854 enhance time:now* placeholders to calculate plus and minus from…
Browse files Browse the repository at this point in the history
… now

* and to optionally truncate to a unit (rounding down)

Signed-off-by: Thomas Jäckle <thomas.jaeckle@beyonnex.io>
  • Loading branch information
thjaeckle committed Jan 4, 2024
1 parent f982ba5 commit e6539b0
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import java.text.MessageFormat;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.function.LongFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -201,15 +203,21 @@ public int hashCode() {
return Objects.hash(amount, dittoTimeUnit);
}

private enum DittoTimeUnit {
/**
* Enumeration providing the supported time units of {@link DittoDuration}, together with their {@link ChronoUnit}.
*
* @since 3.5.0
*/
public enum DittoTimeUnit {

// The order matters as we expect seconds to be the main unit.
// By making it the first constant, parsing a duration from string will be accelerated.
SECONDS("s", Duration::ofSeconds, ChronoUnit.SECONDS),
SECONDS_IMPLICIT("", Duration::ofSeconds, ChronoUnit.SECONDS),
MILLISECONDS("ms", Duration::ofMillis, ChronoUnit.MILLIS),
MINUTES("m", Duration::ofMinutes, ChronoUnit.MINUTES),
HOURS("h", Duration::ofHours, ChronoUnit.HOURS);
HOURS("h", Duration::ofHours, ChronoUnit.HOURS),
DAYS("d", Duration::ofDays, ChronoUnit.DAYS);

private final String suffix;
private final LongFunction<Duration> toJavaDuration;
Expand All @@ -223,19 +231,31 @@ private enum DittoTimeUnit {
regexPattern = Pattern.compile("(?<amount>[+-]?\\d++)(?<unit>" + suffix + ")");
}

private Matcher getRegexMatcher(final CharSequence duration) {
/**
* Find a DittoTimeUnit option by a provided suffix string.
*
* @param suffix the suffix.
* @return the DittoTimeUnit with the given suffix string if any exists.
*/
public static Optional<DittoTimeUnit> forSuffix(final String suffix) {
return Arrays.stream(values())
.filter(unit -> unit.getSuffix().equals(suffix))
.findAny();
}

public Matcher getRegexMatcher(final CharSequence duration) {
return regexPattern.matcher(duration);
}

private String getSuffix() {
public String getSuffix() {
return suffix;
}

private Duration getJavaDuration(final long amount) {
public Duration getJavaDuration(final long amount) {
return toJavaDuration.apply(amount);
}

private ChronoUnit getChronoUnit() {
public ChronoUnit getChronoUnit() {
return chronoUnit;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ public void createDittoDurationFromStringMilliseconds() {
assertThat(dittoDuration.getDuration()).isEqualTo(Duration.ofMillis(durationValue));
}

@Test
public void createDittoDurationFromStringDays() {
final short durationValue = 7;
final DittoDuration dittoDuration = DittoDuration.parseDuration(durationValue + "d");

assertThat(dittoDuration.getDuration()).isEqualTo(Duration.ofDays(durationValue));
}

@Test
public void createDittoDurationFromStringWithAndWithoutSecondsIsEqual() {
final byte durationValue = 23;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public void acceptDittoDurationStringWithNegativeAmount() {

@Test
public void acceptDittoDurationStringWithInvalidTimeUnit() {
final String invalidDittoDurationString = "1d";
final String invalidDittoDurationString = "1w";

assertThatExceptionOfType(DittoHeaderInvalidException.class)
.isThrownBy(() -> underTest.accept(DittoHeaderDefinition.TIMEOUT, invalidDittoDurationString))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public void acceptDittoDurationStringWithNegativeAmount() {

@Test
public void acceptDittoDurationStringWithInvalidTimeUnit() {
final String invalidDittoDurationString = "1d";
final String invalidDittoDurationString = "1y";

assertThatExceptionOfType(TimeoutInvalidException.class)
.isThrownBy(() -> underTest.accept(DittoHeaderDefinition.TIMEOUT, invalidDittoDurationString))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
package org.eclipse.ditto.placeholders;

import java.time.Instant;
import java.util.Arrays;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.List;

import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

import org.eclipse.ditto.base.model.common.ConditionChecker;
import org.eclipse.ditto.base.model.common.DittoDuration;

/**
* Placeholder implementation that replaces:
Expand All @@ -42,8 +44,12 @@ final class ImmutableTimePlaceholder implements TimePlaceholder {
private static final String NOW_PLACEHOLDER = "now";
private static final String NOW_EPOCH_MILLIS_PLACEHOLDER = "now_epoch_millis";

private static final List<String> SUPPORTED = Collections.unmodifiableList(
Arrays.asList(NOW_PLACEHOLDER, NOW_EPOCH_MILLIS_PLACEHOLDER));
private static final String TRUNCATE_START;
private static final String TRUNCATE_END = "]";

static {
TRUNCATE_START = "[";
}

private ImmutableTimePlaceholder() {
}
Expand All @@ -55,25 +61,148 @@ public String getPrefix() {

@Override
public List<String> getSupportedNames() {
return SUPPORTED;
return Collections.emptyList();
}

@Override
public boolean supports(final String name) {
return SUPPORTED.contains(name);
return startsWithNowPrefix(name) && containsEmptyOrValidTimeRangeDefinition(name);
}

@Override
public List<String> resolveValues(final Object someObject, final String placeholder) {
ConditionChecker.argumentNotEmpty(placeholder, "placeholder");
final Instant now = Instant.now();
switch (placeholder) {
case NOW_PLACEHOLDER:
return Collections.singletonList(Instant.now().toString());
return Collections.singletonList(now.toString());
case NOW_EPOCH_MILLIS_PLACEHOLDER:
return Collections.singletonList(String.valueOf(Instant.now().toEpochMilli()));
return Collections.singletonList(formatAsEpochMilli(now));
default:
return Collections.emptyList();
return resolveWithPotentialTimeRangeSuffix(now, placeholder);
}
}

private static boolean startsWithNowPrefix(final String name) {
return name.startsWith(NOW_EPOCH_MILLIS_PLACEHOLDER) || name.startsWith(NOW_PLACEHOLDER) ;
}

private boolean containsEmptyOrValidTimeRangeDefinition(final String placeholder) {
final String timeRangeSuffix = extractTimeRangeSuffix(placeholder);
return timeRangeSuffix.isEmpty() || isValidTimeRange(timeRangeSuffix);
}

private static String formatAsEpochMilli(final Instant now) {
return String.valueOf(now.toEpochMilli());
}

private static String extractTimeRangeSuffix(final String placeholder) {
final String timeRangeSuffix;
if (placeholder.startsWith(NOW_EPOCH_MILLIS_PLACEHOLDER)) {
timeRangeSuffix = placeholder.replace(NOW_EPOCH_MILLIS_PLACEHOLDER, "");
} else if (placeholder.startsWith(NOW_PLACEHOLDER)) {
timeRangeSuffix = placeholder.replace(NOW_PLACEHOLDER, "");
} else {
throw new IllegalStateException("Unsupported placeholder prefix for TimePlaceholder: " + placeholder);
}
return timeRangeSuffix;
}

private boolean isValidTimeRange(final String timeRangeSuffix) {
final char sign = timeRangeSuffix.charAt(0);
if (sign == '-' || sign == '+') {
final String durationWithTruncate = timeRangeSuffix.substring(1);
final String durationString;
if (durationWithTruncate.contains(TRUNCATE_START) && durationWithTruncate.contains(TRUNCATE_END)) {
final String[] durationStringAndTruncateString = durationWithTruncate.split(TRUNCATE_START, 2);
durationString = durationStringAndTruncateString[0];
final String truncateString = durationStringAndTruncateString[1];
if (!isValidTruncateStatement(truncateString.substring(0, truncateString.lastIndexOf(TRUNCATE_END)))) {
return false;
}
} else {
durationString = durationWithTruncate;
}
try {
DittoDuration.parseDuration(durationString);
return true;
} catch (final Exception e) {
return false;
}
} else {
return false;
}
}

private boolean isValidTruncateStatement(final String truncateString) {
return DittoDuration.DittoTimeUnit.forSuffix(truncateString).isPresent();
}

private List<String> resolveWithPotentialTimeRangeSuffix(final Instant now, final String placeholder) {
final String timeRangeSuffix = extractTimeRangeSuffix(placeholder);
if (timeRangeSuffix.isEmpty()) {
return Collections.emptyList();
}

@Nullable final ChronoUnit truncateTo = calculateTruncateTo(timeRangeSuffix);

final char sign = timeRangeSuffix.charAt(0);
if (sign == '-') {
final DittoDuration dittoDuration = extractTimeRangeDuration(timeRangeSuffix);
Instant nowMinus = now.minus(dittoDuration.getDuration());
if (truncateTo != null) {
nowMinus = nowMinus.truncatedTo(truncateTo);
}
return buildResult(placeholder, nowMinus);
} else if (sign == '+') {
final DittoDuration dittoDuration = extractTimeRangeDuration(timeRangeSuffix);
Instant nowPlus = now.plus(dittoDuration.getDuration());
if (truncateTo != null) {
nowPlus = nowPlus.truncatedTo(truncateTo);
}
return buildResult(placeholder, nowPlus);
} else if (truncateTo != null) {
final Instant nowTruncated = now.truncatedTo(truncateTo);
return buildResult(placeholder, nowTruncated);
}

return Collections.emptyList();
}

private static List<String> buildResult(final String placeholder, final Instant nowMinus) {
if (placeholder.startsWith(NOW_EPOCH_MILLIS_PLACEHOLDER)) {
return Collections.singletonList(formatAsEpochMilli(nowMinus));
} else if (placeholder.startsWith(NOW_PLACEHOLDER)) {
return Collections.singletonList(nowMinus.toString());
} else {
return Collections.emptyList();
}
}

@Nullable
private static ChronoUnit calculateTruncateTo(final String timeRangeSuffix) {
if (timeRangeSuffix.contains(TRUNCATE_START) && timeRangeSuffix.contains(TRUNCATE_END)) {
final String truncateUnit = timeRangeSuffix.substring(timeRangeSuffix.indexOf(TRUNCATE_START) + 1,
timeRangeSuffix.lastIndexOf(TRUNCATE_END));
final DittoDuration.DittoTimeUnit dittoTimeUnit =
DittoDuration.DittoTimeUnit.forSuffix(truncateUnit).orElseThrow(() ->
new IllegalStateException("Truncating string contained unsupported unit: " + truncateUnit)
);
return dittoTimeUnit.getChronoUnit();
} else {
return null;
}
}

private static DittoDuration extractTimeRangeDuration(final String timeRangeSuffix) {
final int truncateStart = timeRangeSuffix.indexOf(TRUNCATE_START);
final String timeRange;
if (truncateStart > 0) {
timeRange = timeRangeSuffix.substring(0, truncateStart);
} else {
timeRange = timeRangeSuffix;
}
return DittoDuration.parseDuration(timeRange.substring(1));
}

@Override
Expand Down

0 comments on commit e6539b0

Please sign in to comment.