Skip to content

Commit

Permalink
[eclipse-ditto#964] add "notifyBefore" to SubjectExpiry.
Browse files Browse the repository at this point in the history
Signed-off-by: Yufei Cai <yufei.cai@bosch.io>
  • Loading branch information
yufei-cai committed Feb 14, 2021
1 parent 2431fad commit 7c86a72
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.text.MessageFormat;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
import java.util.function.LongFunction;
import java.util.regex.Matcher;
Expand All @@ -26,12 +27,12 @@
import javax.annotation.concurrent.Immutable;

/**
* Package internal representation of a string based duration with a positive amount.
* Representation of a string based duration with a positive amount.
*
* @since 1.1.0
* @since 2.0.0
*/
@Immutable
final class DittoDuration implements CharSequence {
public final class DittoDuration implements CharSequence {

private final long amount;
private final DittoTimeUnit dittoTimeUnit;
Expand All @@ -49,7 +50,7 @@ private DittoDuration(final long amount, final DittoTimeUnit dittoTimeUnit) {
* @throws NullPointerException if {@code duration} is {@code null}.
* @throws IllegalArgumentException if {@code duration} is negative.
*/
static DittoDuration of(final Duration duration) {
public static DittoDuration of(final Duration duration) {
checkNotNull(duration, "duration");
checkArgument(duration, d -> !d.isNegative(),
() -> MessageFormat.format("The duration must not be negative but was <{0}>!", duration));
Expand All @@ -70,10 +71,10 @@ static DittoDuration of(final Duration duration) {
* @param duration the CharSequence containing the DittoDuration representation to be parsed.
* @return the created DittoDuration.
* @throws NullPointerException if {@code duration} is {@code null}.
* @throws java.lang.NumberFormatException if the duration char sequence does not contain a parsable {@code long}.
* @throws IllegalArgumentException if the parsed duration is not negative.
* @throws IllegalArgumentException if the duration char sequence does not contain a parsable {@code long} or
* the parsed duration is not negative.
*/
static DittoDuration parseDuration(final CharSequence duration) {
public static DittoDuration parseDuration(final CharSequence duration) {
return parseDuration(checkNotNull(duration, "duration"), DittoTimeUnit.values());
}

Expand All @@ -87,7 +88,6 @@ private static DittoDuration parseDuration(final CharSequence duration, final Di
i++;
}
if (null == durationValue) {

// implicitly interpret duration as seconds if unit was omitted
timeUnit = DittoTimeUnit.SECONDS_IMPLICIT;
durationValue = parseDurationPlain(duration, timeUnit.getSuffix());
Expand All @@ -106,6 +106,7 @@ private static Long parseDurationRegexBased(final CharSequence duration, final D
}

private static Long parseDurationPlain(final CharSequence charSequence, final CharSequence timeUnitSuffix) {
// throws NumberFormatException which is a subclass of IllegalArgumentException
final long result = Long.parseLong(charSequence.toString());
checkArgument(result, r -> 0 <= r, () -> {
final String msgPattern = "The duration must not be negative but was <{0}{1}>!";
Expand All @@ -119,7 +120,7 @@ private static Long parseDurationPlain(final CharSequence charSequence, final Ch
*
* @return {@code true} if this duration has a total length equal to zero.
*/
boolean isZero() {
public boolean isZero() {
return 0 == amount;
}

Expand All @@ -128,10 +129,19 @@ boolean isZero() {
*
* @return the Java Duration representation of this DittoDuration.
*/
Duration getDuration() {
public Duration getDuration() {
return dittoTimeUnit.getJavaDuration(amount);
}

/**
* Returns the time unit of the duration.
*
* @return the time unit.
*/
public ChronoUnit getChronoUnit() {
return dittoTimeUnit.getChronoUnit();
}

@Override
public int length() {
return toString().length();
Expand Down Expand Up @@ -173,33 +183,40 @@ private 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),
SECONDS_IMPLICIT("", Duration::ofSeconds),
MILLISECONDS("ms", Duration::ofMillis),
MINUTES("m", Duration::ofMinutes);
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);

private final String suffix;
private final LongFunction<Duration> toJavaDuration;
private final ChronoUnit chronoUnit;
private final Pattern regexPattern;

private DittoTimeUnit(final String suffix, final LongFunction<Duration> toJavaDuration) {
DittoTimeUnit(final String suffix, final LongFunction<Duration> toJavaDuration, final ChronoUnit chronoUnit) {
this.suffix = suffix;
this.toJavaDuration = toJavaDuration;
regexPattern = Pattern.compile("(?<amount>[+-]?\\d+)(?<unit>" + suffix + ")");
this.chronoUnit = chronoUnit;
regexPattern = Pattern.compile("(?<amount>[+-]?\\d++)(?<unit>" + suffix + ")");
}

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

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

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

private ChronoUnit getChronoUnit() {
return chronoUnit;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public static Subject fromJson(final CharSequence subjectIssuerWithId, final Jso
.message("The 'type' for the 'subject' is missing.")
.build()));
final SubjectExpiry subjectExpiry = jsonObject.getValue(JsonFields.EXPIRY)
.map(SubjectExpiry::newInstance)
.map(SubjectExpiry::fromJson)
.orElse(null);

return of(SubjectId.newInstance(subjectIssuerWithId), ImmutableSubjectType.of(subjectTypeValue), subjectExpiry);
Expand Down Expand Up @@ -140,7 +140,7 @@ public JsonObject toJson(final JsonSchemaVersion schemaVersion, final Predicate<
.set(JsonFields.SCHEMA_VERSION, schemaVersion.toInt(), predicate)
.set(JsonFields.TYPE, subjectType.toString(), predicate);
if (null != subjectExpiry) {
jsonObjectBuilder.set(JsonFields.EXPIRY, subjectExpiry.toString());
jsonObjectBuilder.set(JsonFields.EXPIRY, subjectExpiry.toJson());
}
return jsonObjectBuilder.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,19 @@

import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;

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

import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.json.JsonValue;
import org.eclipse.ditto.model.base.headers.DittoDuration;

/**
* An immutable implementation of {@link SubjectExpiry}.
*
Expand All @@ -29,10 +37,16 @@
@Immutable
final class ImmutableSubjectExpiry implements SubjectExpiry {

private static final Set<ChronoUnit> NOTIFY_BEFORE_DURATION_UNITS =
EnumSet.of(ChronoUnit.SECONDS, ChronoUnit.MINUTES, ChronoUnit.HOURS);

private final Instant timestamp;
@Nullable private final DittoDuration notifyBefore;

private ImmutableSubjectExpiry(final Instant timestamp) {
private ImmutableSubjectExpiry(final Instant timestamp,
@Nullable final DittoDuration notifyBefore) {
this.timestamp = timestamp;
this.notifyBefore = notifyBefore;
}

/**
Expand All @@ -46,17 +60,14 @@ private ImmutableSubjectExpiry(final Instant timestamp) {
public static SubjectExpiry of(final CharSequence expiry) {
if (expiry instanceof SubjectExpiry) {
return (SubjectExpiry) expiry;
} else {
return parseAndValidate(expiry, null);
}
}

final Instant timestamp;
try {
timestamp = Instant.parse(checkNotNull(expiry, "expiry"));
} catch (final DateTimeParseException e) {
throw SubjectExpiryInvalidException.newBuilder(expiry)
.cause(e)
.build();
}
return new ImmutableSubjectExpiry(timestamp);
static ImmutableSubjectExpiry parseAndValidate(final CharSequence expiry,
@Nullable final CharSequence notifyBefore) {
return new ImmutableSubjectExpiry(parseExpiryInstant(expiry), parseNotifyBeforeDuration(notifyBefore));
}

/**
Expand All @@ -67,7 +78,18 @@ public static SubjectExpiry of(final CharSequence expiry) {
* @throws NullPointerException if {@code expiry} is {@code null}.
*/
public static SubjectExpiry of(final Instant expiry) {
return new ImmutableSubjectExpiry(checkNotNull(expiry, "expiry"));
return new ImmutableSubjectExpiry(checkNotNull(expiry, "expiry"), null);
}

static SubjectExpiry fromJson(final JsonValue jsonValue) {
if (jsonValue.isString()) {
return of(jsonValue.asString());
} else {
final JsonObject jsonObject = jsonValue.asObject();
final String timestamp = jsonObject.getValueOrThrow(JsonFields.TIMESTAMP);
final String notifyBefore = jsonObject.getValue(JsonFields.NOTIFY_BEFORE).orElse(null);
return parseAndValidate(timestamp, notifyBefore);
}
}

@Override
Expand All @@ -80,6 +102,18 @@ public boolean isExpired() {
return timestamp.isBefore(Instant.now());
}

@Override
public JsonValue toJson() {
if (notifyBefore == null) {
return JsonValue.of(timestamp.toString());
} else {
return JsonObject.newBuilder()
.set(JsonFields.TIMESTAMP, timestamp.toString())
.set(JsonFields.NOTIFY_BEFORE, notifyBefore.toString())
.build();
}
}

@Override
public int length() {
return toString().length();
Expand All @@ -104,17 +138,46 @@ public boolean equals(final Object o) {
return false;
}
final ImmutableSubjectExpiry that = (ImmutableSubjectExpiry) o;
return Objects.equals(timestamp, that.timestamp);
return Objects.equals(timestamp, that.timestamp) && Objects.equals(notifyBefore, that.notifyBefore);
}

@Override
public int hashCode() {
return Objects.hash(timestamp);
return Objects.hash(timestamp, notifyBefore);
}

@Override
@Nonnull
public String toString() {
return timestamp.toString();
return toJson().formatAsString();
}

private static Instant parseExpiryInstant(final CharSequence expiry) {
try {
return Instant.parse(checkNotNull(expiry, "expiry"));
} catch (final DateTimeParseException e) {
throw SubjectExpiryInvalidException.newBuilder(expiry)
.cause(e)
.build();
}
}

@Nullable
private static DittoDuration parseNotifyBeforeDuration(final @Nullable CharSequence notifyBefore) {
if (notifyBefore == null) {
return null;
}
try {
return validateNotifyBeforeDuration(DittoDuration.parseDuration(notifyBefore));
} catch (final NullPointerException | IllegalArgumentException e) {
throw SubjectExpiryInvalidException.newBuilderForNotifyBefore(notifyBefore).build();
}
}

private static DittoDuration validateNotifyBeforeDuration(final DittoDuration dittoDuration) {
if (!NOTIFY_BEFORE_DURATION_UNITS.contains(dittoDuration.getChronoUnit())) {
throw SubjectExpiryInvalidException.newBuilderForNotifyBefore(dittoDuration).build();
}
return dittoDuration;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.eclipse.ditto.json.JsonFieldDefinition;
import org.eclipse.ditto.json.JsonFieldSelector;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.json.JsonValue;
import org.eclipse.ditto.model.base.json.FieldType;
import org.eclipse.ditto.model.base.json.JsonSchemaVersion;
import org.eclipse.ditto.model.base.json.Jsonifiable;
Expand Down Expand Up @@ -178,10 +179,11 @@ final class JsonFields {

/**
* JSON field containing the Subject's expiry time.
*
* @since 2.0.0
*/
public static final JsonFieldDefinition<String> EXPIRY =
JsonFactory.newStringFieldDefinition("expiry", FieldType.REGULAR, JsonSchemaVersion.V_2);
public static final JsonFieldDefinition<JsonValue> EXPIRY =
JsonFactory.newJsonValueFieldDefinition("expiry", FieldType.REGULAR, JsonSchemaVersion.V_2);

private JsonFields() {
throw new AssertionError();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@

import javax.annotation.concurrent.Immutable;

import org.eclipse.ditto.json.JsonFactory;
import org.eclipse.ditto.json.JsonFieldDefinition;
import org.eclipse.ditto.json.JsonValue;
import org.eclipse.ditto.model.base.json.FieldType;
import org.eclipse.ditto.model.base.json.JsonSchemaVersion;

/**
* Represents a {@link Subject} expiry timestamp indicating the instant when a Subject is automatically removed from a
* Policy entry.
Expand Down Expand Up @@ -48,6 +54,10 @@ static SubjectExpiry newInstance(final Instant expiry) {
return PoliciesModelFactory.newSubjectExpiry(expiry);
}

static SubjectExpiry fromJson(final JsonValue jsonValue) {
return ImmutableSubjectExpiry.fromJson(jsonValue);
}

/**
* Returns the timestamp of the expiry as Instant.
*
Expand All @@ -62,6 +72,14 @@ static SubjectExpiry newInstance(final Instant expiry) {
*/
boolean isExpired();

/**
* Returns a JSON representation of the subject expiry.
*
* @return the JSON representation.
* @since 2.0.0
*/
JsonValue toJson();

/**
* Returns the ISO-8601 representation of this expiry timestamp.
*
Expand All @@ -74,4 +92,20 @@ static SubjectExpiry newInstance(final Instant expiry) {
default int compareTo(final SubjectExpiry o) {
return getTimestamp().compareTo(o.getTimestamp());
}

final class JsonFields {

public static final JsonFieldDefinition<String> TIMESTAMP =
JsonFactory.newStringFieldDefinition("timestamp", FieldType.REGULAR, JsonSchemaVersion.V_2);

/**
* JSON field containing the duration before the subject's expiration when a notification is sent to
* the expiring subjects.
*
* @since 2.0.0
*/
public static final JsonFieldDefinition<String> NOTIFY_BEFORE =
JsonFactory.newStringFieldDefinition("notifyBefore", FieldType.REGULAR, JsonSchemaVersion.V_2);

}
}

0 comments on commit 7c86a72

Please sign in to comment.