Skip to content

Commit d58809d

Browse files
Improve date handling
1 parent 5d58791 commit d58809d

File tree

5 files changed

+105
-65
lines changed

5 files changed

+105
-65
lines changed

internal-api/src/main/java/datadog/trace/api/featureflag/ufc/v1/Allocation.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
package datadog.trace.api.featureflag.ufc.v1;
22

3+
import java.util.Date;
34
import java.util.List;
45

56
public class Allocation {
67
public final String key;
78
public final List<Rule> rules;
8-
public final String startAt;
9-
public final String endAt;
9+
public final Date startAt;
10+
public final Date endAt;
1011
public final List<Split> splits;
1112
public final Boolean doLog;
1213

1314
public Allocation(
1415
final String key,
1516
final List<Rule> rules,
16-
final String startAt,
17-
final String endAt,
17+
final Date startAt,
18+
final Date endAt,
1819
final List<Split> splits,
1920
final Boolean doLog) {
2021
this.key = key;

products/feature-flagging/api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,6 @@
2525
import java.nio.charset.StandardCharsets;
2626
import java.security.MessageDigest;
2727
import java.security.NoSuchAlgorithmException;
28-
import java.time.Instant;
29-
import java.time.ZoneOffset;
30-
import java.time.format.DateTimeFormatter;
31-
import java.time.format.DateTimeParseException;
32-
import java.time.temporal.TemporalAccessor;
3328
import java.util.AbstractMap;
3429
import java.util.Date;
3530
import java.util.Deque;
@@ -49,10 +44,6 @@
4944
class DDEvaluator implements Evaluator, FeatureFlaggingGateway.ConfigListener {
5045

5146
private static final Logger LOGGER = LoggerFactory.getLogger(DDEvaluator.class);
52-
private static final List<DateTimeFormatter> DATE_FORMATTERS =
53-
asList(
54-
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneOffset.UTC),
55-
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC));
5647
private static final Set<Class<?>> SUPPORTED_RESOLUTION_TYPES =
5748
new HashSet<>(asList(String.class, Boolean.class, Integer.class, Double.class, Value.class));
5849

@@ -204,12 +195,12 @@ private static boolean isEmpty(final List<?> list) {
204195
}
205196

206197
private static boolean isAllocationActive(final Allocation allocation, final Date now) {
207-
final Date startDate = parseDate(allocation.startAt);
198+
final Date startDate = allocation.startAt;
208199
if (startDate != null && now.before(startDate)) {
209200
return false;
210201
}
211202

212-
final Date endDate = parseDate(allocation.endAt);
203+
final Date endDate = allocation.endAt;
213204
if (endDate != null && now.after(endDate)) {
214205
return false;
215206
}
@@ -388,22 +379,6 @@ private static <T> ProviderEvaluation<T> resolveVariant(
388379
return result;
389380
}
390381

391-
static Date parseDate(final String dateString) {
392-
if (dateString == null) {
393-
return null;
394-
}
395-
for (final DateTimeFormatter formatter : DATE_FORMATTERS) {
396-
try {
397-
final TemporalAccessor temporalAccessor = formatter.parse(dateString);
398-
final Instant instant = Instant.from(temporalAccessor);
399-
return Date.from(instant);
400-
} catch (DateTimeParseException e) {
401-
// ignore it
402-
}
403-
}
404-
return null;
405-
}
406-
407382
private static Object resolveAttribute(final String name, final EvaluationContext context) {
408383
// Special handling for "id" attribute: if not explicitly provided, use targeting key
409384
if ("id".equals(name) && !context.keySet().contains(name)) {

products/feature-flagging/api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,15 @@
4040
import dev.openfeature.sdk.MutableContext;
4141
import dev.openfeature.sdk.ProviderEvaluation;
4242
import dev.openfeature.sdk.Value;
43+
import java.text.ParseException;
44+
import java.text.SimpleDateFormat;
4345
import java.util.ArrayList;
4446
import java.util.Arrays;
4547
import java.util.Date;
4648
import java.util.HashMap;
4749
import java.util.List;
4850
import java.util.Map;
51+
import java.util.TimeZone;
4952
import org.junit.jupiter.api.AfterEach;
5053
import org.junit.jupiter.api.BeforeEach;
5154
import org.junit.jupiter.api.Test;
@@ -75,36 +78,6 @@ public void tearDown() {
7578
FeatureFlaggingGateway.removeExposureListener(exposureListener);
7679
}
7780

78-
private static Arguments[] dateParsingTestCases() {
79-
return new Arguments[] {
80-
// Valid ISO 8601 formats
81-
Arguments.of("2023-01-01T00:00:00Z", new Date(1672531200000L)), // 2023-01-01 00:00:00 UTC
82-
Arguments.of("2023-12-31T23:59:59Z", new Date(1704067199000L)), // 2023-12-31 23:59:59 UTC
83-
Arguments.of("2024-02-29T12:00:00Z", new Date(1709208000000L)), // Leap year date
84-
Arguments.of("2023-01-01T00:00:00.000Z", new Date(1672531200000L)), // With milliseconds
85-
Arguments.of("2023-06-15T14:30:45.123Z", new Date(1686839445123L)), // With milliseconds
86-
87-
// Non supported formats should return null
88-
Arguments.of("2023-01-01T01:00:00+01:00", null), // UTC+1
89-
Arguments.of("2023-01-01T00:00:00-05:00", null), // UTC-5
90-
Arguments.of("2023-01-01", null), // Date only
91-
Arguments.of("invalid-date", null),
92-
Arguments.of("", null),
93-
Arguments.of("not-a-date", null),
94-
Arguments.of("2023/01/01T00:00:00Z", null), // Wrong separator
95-
96-
// Null input
97-
Arguments.of(null, null)
98-
};
99-
}
100-
101-
@MethodSource("dateParsingTestCases")
102-
@ParameterizedTest
103-
public void testDateParsing(final String date, final Object expected) {
104-
final Date value = DDEvaluator.parseDate(date);
105-
assertThat(value, equalTo(expected));
106-
}
107-
10881
private static Arguments[] valueMappingTestCases() {
10982
return new Arguments[] {
11083
// String mappings
@@ -684,7 +657,12 @@ private Flag createTimeBasedFlag() {
684657
final List<Allocation> allocations =
685658
singletonList(
686659
new Allocation(
687-
"time-alloc", null, "2022-01-01T00:00:00Z", "2022-12-31T23:59:59Z", splits, false));
660+
"time-alloc",
661+
null,
662+
parseDate("2022-01-01T00:00:00Z"),
663+
parseDate("2022-12-31T23:59:59Z"),
664+
splits,
665+
false));
688666

689667
return new Flag("time-based-flag", true, ValueType.STRING, variants, allocations);
690668
}
@@ -1087,7 +1065,8 @@ private Flag createFutureAllocationFlag() {
10871065

10881066
// Allocation that starts in the future (2050)
10891067
final Allocation allocation =
1090-
new Allocation("future-alloc", null, "2050-01-01T00:00:00Z", null, splits, false);
1068+
new Allocation(
1069+
"future-alloc", null, parseDate("2050-01-01T00:00:00Z"), null, splits, false);
10911070

10921071
return new Flag(
10931072
"future-allocation-flag", true, ValueType.STRING, variants, singletonList(allocation));
@@ -1220,4 +1199,14 @@ private static Map<String, Object> mapOf(final Object... props) {
12201199
}
12211200
return result;
12221201
}
1202+
1203+
private static Date parseDate(String dateString) {
1204+
try {
1205+
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
1206+
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
1207+
return formatter.parse(dateString);
1208+
} catch (ParseException e) {
1209+
throw new RuntimeException("Failed to parse date: " + dateString, e);
1210+
}
1211+
}
12231212
}

products/feature-flagging/lib/src/main/java/com/datadog/featureflag/RemoteConfigServiceImpl.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.datadog.featureflag;
22

33
import com.squareup.moshi.JsonAdapter;
4+
import com.squareup.moshi.JsonReader;
5+
import com.squareup.moshi.JsonWriter;
46
import com.squareup.moshi.Moshi;
57
import datadog.communication.ddagent.SharedCommunicationObjects;
68
import datadog.remoteconfig.Capabilities;
@@ -14,6 +16,13 @@
1416
import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration;
1517
import java.io.ByteArrayInputStream;
1618
import java.io.IOException;
19+
import java.io.UnsupportedEncodingException;
20+
import java.time.Instant;
21+
import java.time.ZoneOffset;
22+
import java.time.format.DateTimeFormatter;
23+
import java.time.temporal.TemporalAccessor;
24+
import java.util.Date;
25+
import javax.annotation.Nonnull;
1726
import javax.annotation.Nullable;
1827
import okio.Okio;
1928

@@ -55,11 +64,43 @@ static class UniversalFlagConfigDeserializer
5564
static final UniversalFlagConfigDeserializer INSTANCE = new UniversalFlagConfigDeserializer();
5665

5766
private static final JsonAdapter<ServerConfiguration> V1_ADAPTER =
58-
new Moshi.Builder().build().adapter(ServerConfiguration.class);
67+
new Moshi.Builder()
68+
.add(Date.class, new DateAdapter())
69+
.build()
70+
.adapter(ServerConfiguration.class);
5971

6072
@Override
6173
public ServerConfiguration deserialize(final byte[] content) throws IOException {
6274
return V1_ADAPTER.fromJson(Okio.buffer(Okio.source(new ByteArrayInputStream(content))));
6375
}
6476
}
77+
78+
static class DateAdapter extends JsonAdapter<Date> {
79+
80+
private static final DateTimeFormatter DATE_TIME_FORMATTER =
81+
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.SSS]'Z'").withZone(ZoneOffset.UTC);
82+
83+
@Nullable
84+
@Override
85+
public Date fromJson(@Nonnull final JsonReader reader) throws IOException {
86+
final String date = reader.nextString();
87+
if (date == null) {
88+
return null;
89+
}
90+
try {
91+
final TemporalAccessor temporalAccessor = DATE_TIME_FORMATTER.parse(date);
92+
final Instant instant = Instant.from(temporalAccessor);
93+
return Date.from(instant);
94+
} catch (Exception e) {
95+
// ignore wrongly set dates
96+
return null;
97+
}
98+
}
99+
100+
@Override
101+
public void toJson(@Nonnull final JsonWriter writer, @Nullable final Date value)
102+
throws IOException {
103+
throw new UnsupportedEncodingException("Reading only adapter");
104+
}
105+
}
65106
}

products/feature-flagging/lib/src/test/groovy/com/datadog/featureflag/RemoteConfigServiceTest.groovy

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.datadog.featureflag
22

3-
3+
import com.squareup.moshi.JsonReader
44
import datadog.communication.ddagent.SharedCommunicationObjects
55
import datadog.remoteconfig.Capabilities
66
import datadog.remoteconfig.ConfigurationDeserializer
@@ -71,4 +71,38 @@ class RemoteConfigServiceTest extends DDSpecification {
7171
cleanup:
7272
FeatureFlaggingGateway.removeConfigListener(listener)
7373
}
74+
75+
void 'test date parsing'() {
76+
given:
77+
final reader = Stub(JsonReader) {
78+
nextString() >> string
79+
}
80+
final adapter = new RemoteConfigServiceImpl.DateAdapter()
81+
82+
when:
83+
final date = adapter.fromJson(reader)
84+
85+
then:
86+
date == expected
87+
88+
where:
89+
string | expected
90+
// Valid ISO 8601 formats
91+
"2023-01-01T00:00:00Z" | new Date(1672531200000L) // 2023-01-01 00:00:00 UTC
92+
"2023-12-31T23:59:59Z" | new Date(1704067199000L) // 2023-12-31 23:59:59 UTC
93+
"2024-02-29T12:00:00Z" | new Date(1709208000000L) // Leap year date
94+
"2023-01-01T00:00:00.000Z" | new Date(1672531200000L) // With milliseconds
95+
"2023-06-15T14:30:45.123Z" | new Date(1686839445123L) // With milliseconds
96+
// Non supported formats should return null
97+
"2023-01-01T01:00:00+01:00" | null // UTC+1
98+
"2023-01-01T00:00:00-05:00" | null // UTC-5
99+
"2023-01-01" | null // Date only
100+
"invalid-date" | null
101+
"" | null
102+
"not-a-date" | null
103+
"2023/01/01T00:00:00Z" | null // Wrong separator
104+
105+
// Null input
106+
null | null
107+
}
74108
}

0 commit comments

Comments
 (0)