diff --git a/CHANGELOG.md b/CHANGELOG.md index c737fa51b..f8c529e4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +## [1.0.2] - 2024-02-13 + +### Changed + +- Add default UTC offset when deserializing to OffsetDateTime fails due to a missing time offset value. + ## [1.0.1] - 2024-02-09 ### Changed diff --git a/components/serialization/form/gradle/dependencies.gradle b/components/serialization/form/gradle/dependencies.gradle index e4a48c0a0..0b5764103 100644 --- a/components/serialization/form/gradle/dependencies.gradle +++ b/components/serialization/form/gradle/dependencies.gradle @@ -1,6 +1,7 @@ dependencies { // Use JUnit Jupiter API for testing. testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2' // Use JUnit Jupiter Engine for testing. testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' diff --git a/components/serialization/form/src/main/java/com/microsoft/kiota/serialization/FormParseNode.java b/components/serialization/form/src/main/java/com/microsoft/kiota/serialization/FormParseNode.java index d37a3fd73..92928deb3 100644 --- a/components/serialization/form/src/main/java/com/microsoft/kiota/serialization/FormParseNode.java +++ b/components/serialization/form/src/main/java/com/microsoft/kiota/serialization/FormParseNode.java @@ -10,8 +10,11 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -167,7 +170,17 @@ private String sanitizeKey(@Nonnull final String key) { @Nullable public OffsetDateTime getOffsetDateTimeValue() { final String stringValue = getStringValue(); if (stringValue == null) return null; - return OffsetDateTime.parse(stringValue); + try { + return OffsetDateTime.parse(stringValue); + } catch (DateTimeParseException ex) { + // Append UTC offset if it's missing + try { + LocalDateTime localDateTime = LocalDateTime.parse(stringValue); + return localDateTime.atOffset(ZoneOffset.UTC); + } catch (DateTimeParseException ex2) { + throw ex; + } + } } @Nullable public LocalDate getLocalDateValue() { diff --git a/components/serialization/form/src/test/java/com/microsoft/kiota/serialization/ParseNodeTests.java b/components/serialization/form/src/test/java/com/microsoft/kiota/serialization/ParseNodeTests.java index 749b8e889..586507b87 100644 --- a/components/serialization/form/src/test/java/com/microsoft/kiota/serialization/ParseNodeTests.java +++ b/components/serialization/form/src/test/java/com/microsoft/kiota/serialization/ParseNodeTests.java @@ -1,6 +1,7 @@ package com.microsoft.kiota.serialization; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -10,8 +11,13 @@ import com.microsoft.kiota.serialization.mocks.TestEntity; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.time.LocalTime; +import java.time.format.DateTimeParseException; import java.util.List; import java.util.UUID; @@ -99,4 +105,34 @@ public void getCollectionOfGuidPrimitiveValuesFromForm() { assertEquals( UUID.fromString("48d31887-5fad-4d73-a9f5-3c356e68a038"), numberCollection.get(0)); } + + @Test + void testParsesDateTimeOffset() { + final var dateTimeOffsetString = "2024-02-12T19:47:39+02:00"; + final var result = + new FormParseNode(URLEncoder.encode(dateTimeOffsetString, StandardCharsets.UTF_8)) + .getOffsetDateTimeValue(); + assertEquals(dateTimeOffsetString, result.toString()); + } + + @Test + void testParsesDateTimeStringWithoutOffsetToDateTimeOffset() { + final var dateTimeString = "2024-02-12T19:47:39"; + final var result = + new FormParseNode(URLEncoder.encode(dateTimeString, StandardCharsets.UTF_8)) + .getOffsetDateTimeValue(); + assertEquals(dateTimeString + "Z", result.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"2024-02-12T19:47:39 Europe/Paris", "19:47:39"}) + void testInvalidOffsetDateTimeStringThrowsException(final String dateTimeString) { + try { + new FormParseNode(URLEncoder.encode(dateTimeString, StandardCharsets.UTF_8)) + .getOffsetDateTimeValue(); + } catch (final Exception ex) { + assertInstanceOf(DateTimeParseException.class, ex); + assertTrue(ex.getMessage().contains(dateTimeString)); + } + } } diff --git a/components/serialization/json/gradle/dependencies.gradle b/components/serialization/json/gradle/dependencies.gradle index aa95581f0..fb5a781d0 100644 --- a/components/serialization/json/gradle/dependencies.gradle +++ b/components/serialization/json/gradle/dependencies.gradle @@ -1,6 +1,7 @@ dependencies { // Use JUnit Jupiter API for testing. testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2' // Use JUnit Jupiter Engine for testing. testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java index c543c97bc..d029b2185 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java @@ -11,8 +11,11 @@ import java.math.BigDecimal; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Base64; import java.util.EnumSet; @@ -95,7 +98,17 @@ public JsonParseNode(@Nonnull final JsonElement node) { @Nullable public OffsetDateTime getOffsetDateTimeValue() { final String stringValue = currentNode.getAsString(); if (stringValue == null) return null; - return OffsetDateTime.parse(stringValue); + try { + return OffsetDateTime.parse(stringValue); + } catch (DateTimeParseException ex) { + // Append UTC offset if it's missing + try { + LocalDateTime localDateTime = LocalDateTime.parse(stringValue); + return localDateTime.atOffset(ZoneOffset.UTC); + } catch (DateTimeParseException ex2) { + throw ex; + } + } } @Nullable public LocalDate getLocalDateValue() { diff --git a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonParseNodeTests.java b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonParseNodeTests.java index 6f19e7ac2..8a268e560 100644 --- a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonParseNodeTests.java +++ b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonParseNodeTests.java @@ -1,11 +1,19 @@ package com.microsoft.kiota.serialization; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.JsonParser; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.io.ByteArrayInputStream; import java.io.UnsupportedEncodingException; +import java.time.format.DateTimeParseException; class JsonParseNodeTests { private static final JsonParseNodeFactory _parseNodeFactory = new JsonParseNodeFactory(); @@ -19,4 +27,32 @@ void itDDoesNotFailForGetChildElementOnMissingKey() throws UnsupportedEncodingEx final var result = parseNode.getChildNode("@odata.type"); assertNull(result); } + + @Test + void testParsesDateTimeOffset() { + final var dateTimeOffsetString = "2024-02-12T19:47:39+02:00"; + final var jsonElement = JsonParser.parseString("\"" + dateTimeOffsetString + "\""); + final var result = new JsonParseNode(jsonElement).getOffsetDateTimeValue(); + assertEquals(dateTimeOffsetString, result.toString()); + } + + @Test + void testParsesDateTimeStringWithoutOffsetToDateTimeOffset() { + final var dateTimeString = "2024-02-12T19:47:39"; + final var jsonElement = JsonParser.parseString("\"" + dateTimeString + "\""); + final var result = new JsonParseNode(jsonElement).getOffsetDateTimeValue(); + assertEquals(dateTimeString + "Z", result.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"2024-02-12T19:47:39 Europe/Paris", "19:47:39"}) + void testInvalidOffsetDateTimeStringThrowsException(final String dateTimeString) { + final var jsonElement = JsonParser.parseString("\"" + dateTimeString + "\""); + try { + new JsonParseNode(jsonElement).getOffsetDateTimeValue(); + } catch (final Exception ex) { + assertInstanceOf(DateTimeParseException.class, ex); + assertTrue(ex.getMessage().contains(dateTimeString)); + } + } } diff --git a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/UnionWrapperParseTests.java b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/UnionWrapperParseTests.java index f48c4f037..05f576552 100644 --- a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/UnionWrapperParseTests.java +++ b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/UnionWrapperParseTests.java @@ -21,7 +21,7 @@ class UnionWrapperParseTests { private static final String contentType = "application/json"; @Test - void ParsesUnionTypeComplexProperty1() throws UnsupportedEncodingException { + void parsesUnionTypeComplexProperty1() throws UnsupportedEncodingException { final var initialString = "{\"@odata.type\":\"#microsoft.graph.testEntity\",\"officeLocation\":\"Montreal\"," + " \"id\": \"opaque\"}"; @@ -38,7 +38,7 @@ void ParsesUnionTypeComplexProperty1() throws UnsupportedEncodingException { } @Test - void ParsesUnionTypeComplexProperty2() throws UnsupportedEncodingException { + void parsesUnionTypeComplexProperty2() throws UnsupportedEncodingException { final var initialString = "{\"@odata.type\":\"#microsoft.graph.secondTestEntity\",\"officeLocation\":\"Montreal\"," + " \"id\": 10}"; @@ -54,7 +54,7 @@ void ParsesUnionTypeComplexProperty2() throws UnsupportedEncodingException { } @Test - void ParsesUnionTypeComplexProperty3() throws UnsupportedEncodingException { + void parsesUnionTypeComplexProperty3() throws UnsupportedEncodingException { final var initialString = "[{\"@odata.type\":\"#microsoft.graph.TestEntity\",\"officeLocation\":\"Ottawa\"," + " \"id\": \"11\"}," @@ -73,7 +73,7 @@ void ParsesUnionTypeComplexProperty3() throws UnsupportedEncodingException { } @Test - void ParsesUnionTypeStringValue() throws UnsupportedEncodingException { + void parsesUnionTypeStringValue() throws UnsupportedEncodingException { final var initialString = "\"officeLocation\""; final var rawResponse = new ByteArrayInputStream(initialString.getBytes("UTF-8")); final var parseNode = _parseNodeFactory.getParseNode(contentType, rawResponse); @@ -87,7 +87,7 @@ void ParsesUnionTypeStringValue() throws UnsupportedEncodingException { } @Test - void SerializesUnionTypeStringValue() throws IOException { + void serializesUnionTypeStringValue() throws IOException { try (final var writer = _serializationWriterFactory.getSerializationWriter(contentType)) { var model = new UnionTypeMock() { @@ -105,7 +105,7 @@ void SerializesUnionTypeStringValue() throws IOException { } @Test - void SerializesUnionTypeComplexProperty1() throws IOException { + void serializesUnionTypeComplexProperty1() throws IOException { try (final var writer = _serializationWriterFactory.getSerializationWriter(contentType)) { var model = new UnionTypeMock() { @@ -135,7 +135,7 @@ void SerializesUnionTypeComplexProperty1() throws IOException { } @Test - void SerializesUnionTypeComplexProperty2() throws IOException { + void serializesUnionTypeComplexProperty2() throws IOException { try (final var writer = _serializationWriterFactory.getSerializationWriter(contentType)) { var model = new UnionTypeMock() { @@ -159,7 +159,7 @@ void SerializesUnionTypeComplexProperty2() throws IOException { } @Test - void SerializesUnionTypeComplexProperty3() throws IOException { + void serializesUnionTypeComplexProperty3() throws IOException { try (final var writer = _serializationWriterFactory.getSerializationWriter(contentType)) { var model = new UnionTypeMock() { diff --git a/components/serialization/text/gradle/dependencies.gradle b/components/serialization/text/gradle/dependencies.gradle index 5831df1ef..0b5764103 100644 --- a/components/serialization/text/gradle/dependencies.gradle +++ b/components/serialization/text/gradle/dependencies.gradle @@ -1,12 +1,13 @@ dependencies { // Use JUnit Jupiter API for testing. testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2' // Use JUnit Jupiter Engine for testing. testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1' - + api project(':components:abstractions') } diff --git a/components/serialization/text/src/main/java/com/microsoft/kiota/serialization/TextParseNode.java b/components/serialization/text/src/main/java/com/microsoft/kiota/serialization/TextParseNode.java index a7879180b..11ddafc20 100644 --- a/components/serialization/text/src/main/java/com/microsoft/kiota/serialization/TextParseNode.java +++ b/components/serialization/text/src/main/java/com/microsoft/kiota/serialization/TextParseNode.java @@ -7,8 +7,11 @@ import java.math.BigDecimal; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeParseException; import java.util.Base64; import java.util.EnumSet; import java.util.List; @@ -79,7 +82,17 @@ public TextParseNode(@Nonnull final String rawText) { } @Nullable public OffsetDateTime getOffsetDateTimeValue() { - return OffsetDateTime.parse(this.getStringValue()); + try { + return OffsetDateTime.parse(this.getStringValue()); + } catch (DateTimeParseException ex) { + // Append UTC offset if it's missing + try { + LocalDateTime localDateTime = LocalDateTime.parse(this.getStringValue()); + return localDateTime.atOffset(ZoneOffset.UTC); + } catch (DateTimeParseException ex2) { + throw ex; + } + } } @Nullable public LocalDate getLocalDateValue() { diff --git a/components/serialization/text/src/test/java/com/microsoft/kiota/serialization/TextParseNodeTest.java b/components/serialization/text/src/test/java/com/microsoft/kiota/serialization/TextParseNodeTest.java new file mode 100644 index 000000000..6b88e405d --- /dev/null +++ b/components/serialization/text/src/test/java/com/microsoft/kiota/serialization/TextParseNodeTest.java @@ -0,0 +1,39 @@ +package com.microsoft.kiota.serialization; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.time.format.DateTimeParseException; + +public class TextParseNodeTest { + + @Test + void testParsesDateTimeOffset() { + final var dateTimeOffsetString = "2024-02-12T19:47:39+02:00"; + final var result = new TextParseNode(dateTimeOffsetString).getOffsetDateTimeValue(); + assertEquals(dateTimeOffsetString, result.toString()); + } + + @Test + void testParsesDateTimeStringWithoutOffsetToDateTimeOffset() { + final var dateTimeString = "2024-02-12T19:47:39"; + final var result = new TextParseNode(dateTimeString).getOffsetDateTimeValue(); + assertEquals(dateTimeString + "Z", result.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"2024-02-12T19:47:39 Europe/Paris", "19:47:39"}) + void testInvalidOffsetDateTimeStringThrowsException(final String dateTimeString) { + try { + new TextParseNode(dateTimeString).getOffsetDateTimeValue(); + } catch (final Exception ex) { + assertInstanceOf(DateTimeParseException.class, ex); + assertTrue(ex.getMessage().contains(dateTimeString)); + } + } +} diff --git a/gradle.properties b/gradle.properties index cb5ced2f8..405e9078b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,7 @@ org.gradle.caching=true mavenGroupId = com.microsoft.kiota mavenMajorVersion = 1 mavenMinorVersion = 0 -mavenPatchVersion = 1 +mavenPatchVersion = 2 mavenArtifactSuffix = #These values are used to run functional tests