diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/mailgun/v1/EmailsSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/mailgun/v1/EmailsSteps.java index 558af2dcb..028ad8fe7 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/mailgun/v1/EmailsSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/mailgun/v1/EmailsSteps.java @@ -162,13 +162,13 @@ public void getSendingQueuesStatusResult() { ExceededQueueQuota.builder() .setIsDisabled(false) .setDetails( - QueueStatusDisabledDetails.builder().setUntil("").setReason("").build()) + QueueStatusDisabledDetails.builder().setUntil(null).setReason("").build()) .build()) .setScheduled( ExceededQueueQuota.builder() .setIsDisabled(false) .setDetails( - QueueStatusDisabledDetails.builder().setUntil("").setReason("").build()) + QueueStatusDisabledDetails.builder().setUntil(null).setReason("").build()) .build()) .build(); diff --git a/core/src/main/com/sinch/sdk/core/utils/DateUtil.java b/core/src/main/com/sinch/sdk/core/utils/DateUtil.java index d63a77e77..3cc01b2ba 100644 --- a/core/src/main/com/sinch/sdk/core/utils/DateUtil.java +++ b/core/src/main/com/sinch/sdk/core/utils/DateUtil.java @@ -3,12 +3,24 @@ import java.time.Instant; import java.time.LocalDateTime; import java.time.OffsetDateTime; +import java.time.ZoneId; import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import java.util.logging.Logger; /** Utility class for Date */ public class DateUtil { + private static final DateTimeFormatter RFC822_ZONED_FORMAT = + DateTimeFormatter.ofPattern("EEE, d MMM yyyy HH:mm:ss z"); + + private static final DateTimeFormatter RFC822_GMT_FORMAT = + DateTimeFormatter.ofPattern("EEE, d MMM yyyy HH:mm:ss").withZone(ZoneId.of("GMT")); + + private static final Logger LOGGER = Logger.getLogger(DateUtil.class.getName()); + private DateUtil() {} /** @@ -41,20 +53,116 @@ public static Instant toInstant(OffsetDateTime value) { */ public static Instant failSafeTimeStampToInstant(String value) { - if (null == value) { + String trimmed = null == value ? "" : value.trim(); + + if (trimmed.isEmpty()) { + return null; + } + + Instant parsed = parseISO8601(trimmed); + if (null != parsed) { + return parsed; + } + + parsed = parseISO8601WithOffset(trimmed); + if (null != parsed) { + return parsed; + } + + // fallback: this is not an ISO8601 compliant format: give a chance and assume it is GMT zone + parsed = parseISO8601WithoutOffset(trimmed); + if (null != parsed) { + return parsed; + } + + // do not break deserialization: fallback to empty value + LOGGER.severe(String.format("Unable to parse '%s' date string", value)); + + return null; + } + + private static Instant parseISO8601(String trimmed) { + try { + return Instant.parse(trimmed); + } catch (DateTimeParseException _unused) { + return null; + } + } + + private static Instant parseISO8601WithOffset(String trimmed) { + try { + return OffsetDateTime.parse(trimmed).toInstant(); + } catch (DateTimeParseException _unused) { return null; } + } - Instant timestamp; + private static Instant parseISO8601WithoutOffset(String trimmed) { try { - timestamp = Instant.parse(value); - } catch (DateTimeParseException e) { - try { - timestamp = OffsetDateTime.parse(value).toInstant(); - } catch (DateTimeParseException dte) { - timestamp = LocalDateTime.parse(value).toInstant(ZoneOffset.UTC); - } - } - return timestamp; + return LocalDateTime.parse(trimmed).toInstant(ZoneOffset.UTC); + } catch (DateTimeParseException _unused) { + return null; + } + } + + /** + * Convert String to Instant + * + *

Consume a date time in form of RFC-822 format + * + * @param value An RFC-822 compliant string + * @return Extracted Instant value + * @since __TO_BE_DEFINED__ + */ + public static Instant RFC822StringToInstant(String value) { + + String trimmed = null == value ? "" : value.trim(); + + if (trimmed.isEmpty()) { + return null; + } + + Instant parsed = parseRFC822(trimmed); + if (null != parsed) { + return parsed; + } + + parsed = parseRFC822WithZone(trimmed); + if (null != parsed) { + return parsed; + } + + // fallback: this is not an RFC compliant format: give a chance and assume it is GMT zone + parsed = parseRFC822WithoutZoneNorOffset(trimmed); + if (null != parsed) { + return parsed; + } + + LOGGER.severe(String.format("Unable to parse '%s' date string", value)); + return null; + } + + private static Instant parseRFC822(String trimmed) { + try { + return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(trimmed)); + } catch (DateTimeParseException _unused) { + return null; + } + } + + private static Instant parseRFC822WithZone(String trimmed) { + try { + return ZonedDateTime.parse(trimmed, RFC822_ZONED_FORMAT).toInstant(); + } catch (DateTimeParseException _unused) { + return null; + } + } + + private static Instant parseRFC822WithoutZoneNorOffset(String trimmed) { + try { + return ZonedDateTime.parse(trimmed, RFC822_GMT_FORMAT).toInstant(); + } catch (DateTimeParseException _unused) { + return null; + } } } diff --git a/core/src/main/com/sinch/sdk/core/utils/databind/Mapper.java b/core/src/main/com/sinch/sdk/core/utils/databind/Mapper.java index 287c10f18..25b48d047 100644 --- a/core/src/main/com/sinch/sdk/core/utils/databind/Mapper.java +++ b/core/src/main/com/sinch/sdk/core/utils/databind/Mapper.java @@ -34,7 +34,7 @@ public class Mapper { - public static final PropertyFilter uninitializedFilter = + private static final PropertyFilter uninitializedFilter = new SimpleBeanPropertyFilter() { @Override public void serializeAsField( @@ -95,19 +95,19 @@ public static ObjectMapper getInstance() { private static class LazyHolder { - public static final SimpleModule module = + static final SimpleModule module = new JavaTimeModule() .addDeserializer(OffsetDateTime.class, new OffsetDateTimeCustomDeserializer()) .addDeserializer(Instant.class, new InstantCustomDeserializer()); - public static final SimpleModule optionalValueModule = + static final SimpleModule optionalValueModule = new SimpleModule("optionalValueModule") .addSerializer(OptionalValue.class, new OptionalValueSerializer()); - public static final SimpleModule dynamicEnumModule = + static final SimpleModule dynamicEnumModule = new SimpleModule("dynamicEnumModule") .addSerializer(EnumDynamic.class, new EnumDynamicSerializer()); - public static final SimpleModule validationDeserializationModule = + static final SimpleModule validationDeserializationModule = new SimpleModule("deserializationModule") .setDeserializerModifier(new BuilderDeserializerWithValidation()); @@ -128,7 +128,7 @@ private static class LazyHolder { .registerModule(validationDeserializationModule); } - public static final class OffsetDateTimeCustomDeserializer + private static final class OffsetDateTimeCustomDeserializer extends JsonDeserializer { @Override @@ -148,18 +148,33 @@ public OffsetDateTime deserialize(JsonParser parser, DeserializationContext cont } } - public static final class InstantCustomDeserializer extends JsonDeserializer { + private static final class InstantCustomDeserializer extends JsonDeserializer { @Override public Instant deserialize(JsonParser parser, DeserializationContext context) throws IOException { String text = parser.getText(); - return DateUtil.failSafeTimeStampToInstant(text); + + if (null == text) { + return null; + } + + String trimmed = text.trim(); + if (trimmed.isEmpty()) { + return null; + } + + // RFC Date are starting with character not a digit + if (Character.isDigit(text.charAt(0))) { + return DateUtil.failSafeTimeStampToInstant(text); + } + + return DateUtil.RFC822StringToInstant(text); } } - public static class EnumDynamicSerializer extends JsonSerializer { + private static class EnumDynamicSerializer extends JsonSerializer { @Override public void serialize(EnumDynamic value, JsonGenerator jgen, SerializerProvider provider) @@ -168,7 +183,7 @@ public void serialize(EnumDynamic value, JsonGenerator jgen, SerializerProvider } } - public static class OptionalValueSerializer extends JsonSerializer { + private static class OptionalValueSerializer extends JsonSerializer { @Override public void serialize(OptionalValue value, JsonGenerator jgen, SerializerProvider provider) diff --git a/core/src/test/java/com/sinch/sdk/core/utils/DateUtilTest.java b/core/src/test/java/com/sinch/sdk/core/utils/DateUtilTest.java index 85a298886..4ad30d67b 100644 --- a/core/src/test/java/com/sinch/sdk/core/utils/DateUtilTest.java +++ b/core/src/test/java/com/sinch/sdk/core/utils/DateUtilTest.java @@ -32,6 +32,18 @@ void failSafeTimeStampNullGuard() { assertNull(instant); } + @Test + void failSafeTimeStampFromBlankString() { + Instant instant = DateUtil.failSafeTimeStampToInstant(" "); + assertNull(instant); + } + + @Test + void failSafeTimeStampFromStringWithBlanks() { + Instant instant = DateUtil.failSafeTimeStampToInstant(" 2024-05-04T10:00:00.1234 "); + assertEquals("2024-05-04T10:00:00.123400Z", instant.toString()); + } + @Test void failSafeTimeStampNoTZ() { Instant instant = DateUtil.failSafeTimeStampToInstant("2024-05-04T10:00:00.1234"); @@ -49,4 +61,71 @@ void failSafeTimeStampFromUTC() { Instant instant = DateUtil.failSafeTimeStampToInstant("2024-05-04T10:00:00.1234Z"); assertEquals("2024-05-04T10:00:00.123400Z", instant.toString()); } + + @Test + void failSafeTimeStampInvalid() { + Instant instant = DateUtil.failSafeTimeStampToInstant("2024 05 04 10 00 00 1234 "); + assertNull(instant); + } + + @Test + void RFC822NullGuard() { + Instant instant = DateUtil.RFC822StringToInstant(null); + assertNull(instant); + } + + @Test + void RFC822FromBlankString() { + Instant instant = DateUtil.RFC822StringToInstant(" "); + assertNull(instant); + } + + @Test + void RFC822FromStringWithBlanks() { + Instant instant = DateUtil.RFC822StringToInstant(" Mon, 2 Jan 2006 15:04:05 MST "); + assertEquals("2006-01-02T22:04:05Z", instant.toString()); + } + + @Test + void RFC822WithUnsupportedMST() { + Instant instant = DateUtil.RFC822StringToInstant("Mon, 2 Jan 2006 15:04:05 MST"); + assertEquals("2006-01-02T22:04:05Z", instant.toString()); + } + + @Test + void RFC822WithGMT() { + Instant instant = DateUtil.RFC822StringToInstant("Mon, 2 Jan 2006 15:04:05 GMT"); + assertEquals("2006-01-02T15:04:05Z", instant.toString()); + } + + @Test + void RFC822WithUnsupportedUT() { + Instant instant = DateUtil.RFC822StringToInstant("Mon, 2 Jan 2006 15:04:05 UT"); + assertEquals("2006-01-02T15:04:05Z", instant.toString()); + } + + @Test + void RFC822WithUnsupportedEST() { + Instant instant = DateUtil.RFC822StringToInstant("Mon, 2 Jan 2006 15:04:05 EST"); + assertEquals("2006-01-02T20:04:05Z", instant.toString()); + } + + @Test + void RFC822WithOffset() { + Instant instant = DateUtil.RFC822StringToInstant("Mon, 2 Jan 2006 15:04:05 +0100"); + assertEquals("2006-01-02T14:04:05Z", instant.toString()); + } + + @Test + void RFC822NoOffset() { + Instant instant = DateUtil.RFC822StringToInstant("Mon, 2 Jan 2006 15:04:05"); + assertEquals("2006-01-02T15:04:05Z", instant.toString()); + } + + @Test + void RFC822Invalid() { + // 12th of January 2006 is not a Monday (it was a Thursday) + Instant instant = DateUtil.RFC822StringToInstant("Mon, 12 Jan 2006 15:04:05 +0100"); + assertNull(instant); + } } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/response/QueueStatusDisabledDetails.java b/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/response/QueueStatusDisabledDetails.java index b74218998..33bfa283b 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/response/QueueStatusDisabledDetails.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/response/QueueStatusDisabledDetails.java @@ -11,6 +11,7 @@ package com.sinch.sdk.domains.mailgun.models.v1.emails.response; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.time.Instant; /** QueueStatusDisabledDetails */ @JsonDeserialize(builder = QueueStatusDisabledDetailsImpl.Builder.class) @@ -21,7 +22,7 @@ public interface QueueStatusDisabledDetails { * * @return until */ - String getUntil(); + Instant getUntil(); /** * Get reason @@ -49,7 +50,7 @@ interface Builder { * @return Current builder * @see #getUntil */ - Builder setUntil(String until); + Builder setUntil(Instant until); /** * see getter diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/response/QueueStatusDisabledDetailsImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/response/QueueStatusDisabledDetailsImpl.java index 9cbf9124c..ebe4bbb66 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/response/QueueStatusDisabledDetailsImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/mailgun/models/v1/emails/response/QueueStatusDisabledDetailsImpl.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import com.sinch.sdk.core.models.OptionalValue; +import java.time.Instant; import java.util.Objects; @JsonPropertyOrder({ @@ -20,7 +21,7 @@ public class QueueStatusDisabledDetailsImpl implements QueueStatusDisabledDetail public static final String JSON_PROPERTY_UNTIL = "until"; - private OptionalValue until; + private OptionalValue until; public static final String JSON_PROPERTY_REASON = "reason"; @@ -29,19 +30,19 @@ public class QueueStatusDisabledDetailsImpl implements QueueStatusDisabledDetail public QueueStatusDisabledDetailsImpl() {} protected QueueStatusDisabledDetailsImpl( - OptionalValue until, OptionalValue reason) { + OptionalValue until, OptionalValue reason) { this.until = until; this.reason = reason; } @JsonIgnore - public String getUntil() { + public Instant getUntil() { return until.orElse(null); } @JsonProperty(JSON_PROPERTY_UNTIL) @JsonInclude(value = JsonInclude.Include.ALWAYS) - public OptionalValue until() { + public OptionalValue until() { return until; } @@ -103,11 +104,11 @@ private String toIndentedString(Object o) { @JsonPOJOBuilder(withPrefix = "set") static class Builder implements QueueStatusDisabledDetails.Builder { - OptionalValue until = OptionalValue.empty(); + OptionalValue until = OptionalValue.empty(); OptionalValue reason = OptionalValue.empty(); @JsonProperty(JSON_PROPERTY_UNTIL) - public Builder setUntil(String until) { + public Builder setUntil(Instant until) { this.until = OptionalValue.of(until); return this; } diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/mailgun/models/v1/emails/response/SendingQueuesStatusResponseTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/mailgun/models/v1/emails/response/SendingQueuesStatusResponseTest.java index 0205f89cc..005d9afcb 100644 --- a/openapi-contracts/src/test/java/com/sinch/sdk/domains/mailgun/models/v1/emails/response/SendingQueuesStatusResponseTest.java +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/mailgun/models/v1/emails/response/SendingQueuesStatusResponseTest.java @@ -4,6 +4,7 @@ import com.adelean.inject.resources.junit.jupiter.TestWithResources; import com.sinch.sdk.BaseTest; import com.sinch.sdk.core.TestHelpers; +import java.time.Instant; import org.junit.jupiter.api.Test; @TestWithResources @@ -16,7 +17,7 @@ public class SendingQueuesStatusResponseTest extends BaseTest { .setIsDisabled(true) .setDetails( QueueStatusDisabledDetails.builder() - .setUntil("Mon, 24 Jan 2006 16:00:00 MST") + .setUntil(Instant.parse("2025-01-30T04:14:04Z")) .setReason("You have too many messages in regular queue") .build()) .build()) @@ -25,7 +26,7 @@ public class SendingQueuesStatusResponseTest extends BaseTest { .setIsDisabled(true) .setDetails( QueueStatusDisabledDetails.builder() - .setUntil("Mon, 12 Jan 2006 15:04:05 MST") + .setUntil(Instant.parse("2025-01-30T15:14:04Z")) .setReason("You have too many messages in scheduled queue") .build()) .build()) diff --git a/openapi-contracts/src/test/resources/domains/mailgun/v1/emails/response/SendingQueuesStatusResponseDto.json b/openapi-contracts/src/test/resources/domains/mailgun/v1/emails/response/SendingQueuesStatusResponseDto.json index e7cf05986..b4110ce54 100644 --- a/openapi-contracts/src/test/resources/domains/mailgun/v1/emails/response/SendingQueuesStatusResponseDto.json +++ b/openapi-contracts/src/test/resources/domains/mailgun/v1/emails/response/SendingQueuesStatusResponseDto.json @@ -2,14 +2,14 @@ "scheduled": { "is_disabled": true, "disabled": { - "until": "Mon, 12 Jan 2006 15:04:05 MST", + "until": "Thu, 30 Jan 2025 16:14:04 +0100", "reason": "You have too many messages in scheduled queue" } }, "regular": { "is_disabled": true, "disabled": { - "until": "Mon, 24 Jan 2006 16:00:00 MST", + "until": "Thu, 30 Jan 2025 06:14:04 +0200", "reason": "You have too many messages in regular queue" } }