diff --git a/components/serialization/json/spotBugsExcludeFilter.xml b/components/serialization/json/spotBugsExcludeFilter.xml index d0f06c8a..4a40a57f 100644 --- a/components/serialization/json/spotBugsExcludeFilter.xml +++ b/components/serialization/json/spotBugsExcludeFilter.xml @@ -18,6 +18,7 @@ xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubu + @@ -31,4 +32,4 @@ xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubu - \ No newline at end of file + diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java new file mode 100644 index 00000000..f19a651f --- /dev/null +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java @@ -0,0 +1,163 @@ +package com.microsoft.kiota.serialization; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.microsoft.kiota.PeriodAndDuration; + +import jakarta.annotation.Nonnull; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Base64; + +public class DefaultGsonBuilder { + + private static final TypeAdapter OFFSET_DATE_TIME = + new TypeAdapter() { + @Override + public OffsetDateTime read(JsonReader in) throws IOException { + String stringValue = in.nextString(); + 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 new JsonSyntaxException( + "Failed parsing '" + + stringValue + + "' as OffsetDateTime; at path " + + in.getPreviousPath(), + ex2); + } + } + } + + @Override + public void write(JsonWriter out, OffsetDateTime value) throws IOException { + out.value(value.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)); + } + }; + + private static final TypeAdapter LOCAL_DATE = + new TypeAdapter() { + @Override + public LocalDate read(JsonReader in) throws IOException { + String stringValue = in.nextString(); + try { + return LocalDate.parse(stringValue); + } catch (DateTimeParseException ex) { + throw new JsonSyntaxException( + "Failed parsing '" + + stringValue + + "' as LocalDate; at path " + + in.getPreviousPath(), + ex); + } + } + + @Override + public void write(JsonWriter out, LocalDate value) throws IOException { + out.value(value.format(DateTimeFormatter.ISO_LOCAL_DATE)); + } + }; + + private static final TypeAdapter LOCAL_TIME = + new TypeAdapter() { + @Override + public LocalTime read(JsonReader in) throws IOException { + String stringValue = in.nextString(); + try { + return LocalTime.parse(stringValue); + } catch (DateTimeParseException ex) { + throw new JsonSyntaxException( + "Failed parsing '" + + stringValue + + "' as LocalTime; at path " + + in.getPreviousPath(), + ex); + } + } + + @Override + public void write(JsonWriter out, LocalTime value) throws IOException { + out.value(value.format(DateTimeFormatter.ISO_LOCAL_TIME)); + } + }; + + private static final TypeAdapter PERIOD_AND_DURATION = + new TypeAdapter() { + @Override + public PeriodAndDuration read(JsonReader in) throws IOException { + String stringValue = in.nextString(); + try { + return PeriodAndDuration.parse(stringValue); + } catch (DateTimeParseException ex) { + throw new JsonSyntaxException( + "Failed parsing '" + + stringValue + + "' as PeriodAndDuration; at path " + + in.getPreviousPath(), + ex); + } + } + + @Override + public void write(JsonWriter out, PeriodAndDuration value) throws IOException { + out.value(value.toString()); + } + }; + + private static final TypeAdapter BYTE_ARRAY = + new TypeAdapter() { + @Override + public byte[] read(JsonReader in) throws IOException { + String stringValue = in.nextString(); + try { + if (stringValue.isEmpty()) { + return null; + } + return Base64.getDecoder().decode(stringValue); + } catch (IllegalArgumentException ex) { + throw new JsonSyntaxException( + "Failed parsing '" + + stringValue + + "' as byte[]; at path " + + in.getPreviousPath(), + ex); + } + } + + @Override + public void write(JsonWriter out, byte[] value) throws IOException { + out.value(Base64.getEncoder().encodeToString(value)); + } + }; + + private static final Gson defaultInstance = getDefaultBuilder().create(); + + public static @Nonnull Gson getDefaultInstance() { + return defaultInstance; + } + + public static @Nonnull GsonBuilder getDefaultBuilder() { + return new GsonBuilder() + .registerTypeAdapter(OffsetDateTime.class, OFFSET_DATE_TIME.nullSafe()) + .registerTypeAdapter(LocalDate.class, LOCAL_DATE.nullSafe()) + .registerTypeAdapter(LocalTime.class, LOCAL_TIME.nullSafe()) + .registerTypeAdapter(PeriodAndDuration.class, PERIOD_AND_DURATION.nullSafe()) + .registerTypeAdapter(byte[].class, BYTE_ARRAY.nullSafe()); + } +} 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 8ee6e59a..f7b00659 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 @@ -1,5 +1,6 @@ package com.microsoft.kiota.serialization; +import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -11,13 +12,9 @@ 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; import java.util.HashMap; import java.util.Iterator; @@ -31,13 +28,33 @@ /** ParseNode implementation for JSON */ public class JsonParseNode implements ParseNode { private final JsonElement currentNode; + private final Gson gson; /** * Creates a new instance of the JsonParseNode class. * @param node the node to wrap. */ public JsonParseNode(@Nonnull final JsonElement node) { + this(node, DefaultGsonBuilder.getDefaultInstance()); + } + + /** + * Creates a new instance of the JsonParseNode class. + * @param node the node to wrap. + * @param gson the Gson instance to use and deserialize the node with. + */ + public JsonParseNode(@Nonnull final JsonElement node, @Nonnull final Gson gson) { currentNode = Objects.requireNonNull(node, "parameter node cannot be null"); + this.gson = Objects.requireNonNull(gson, "parameter gson cannot be null"); + } + + /** + * Creates a new {@link JsonParseNode} for the given {@link JsonElement}. + * @param node the node to wrap. + * @return the newly created {@link JsonParseNode}. + */ + @Nonnull private JsonParseNode createNewNode(@Nonnull JsonElement node) { + return new JsonParseNode(node, gson); } /** {@inheritDoc} */ @@ -47,7 +64,7 @@ public JsonParseNode(@Nonnull final JsonElement node) { final JsonObject object = currentNode.getAsJsonObject(); final JsonElement childNodeElement = object.get(identifier); if (childNodeElement == null) return null; - final JsonParseNode result = new JsonParseNode(childNodeElement); + final JsonParseNode result = createNewNode(childNodeElement); result.setOnBeforeAssignFieldValues(this.onBeforeAssignFieldValues); result.setOnAfterAssignFieldValues(this.onAfterAssignFieldValues); return result; @@ -55,114 +72,64 @@ public JsonParseNode(@Nonnull final JsonElement node) { } @Nullable public String getStringValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsString() : null; + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, String.class) : null; } @Nullable public Boolean getBooleanValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsBoolean() : null; + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, Boolean.class) : null; } @Nullable public Byte getByteValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsByte() : null; + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, Byte.class) : null; } @Nullable public Short getShortValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsShort() : null; + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, Short.class) : null; } @Nullable public BigDecimal getBigDecimalValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsBigDecimal() : null; + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, BigDecimal.class) : null; } @Nullable public Integer getIntegerValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsInt() : null; + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, Integer.class) : null; } @Nullable public Float getFloatValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsFloat() : null; + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, Float.class) : null; } @Nullable public Double getDoubleValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsDouble() : null; + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, Double.class) : null; } @Nullable public Long getLongValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsLong() : null; + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, Long.class) : null; } @Nullable public UUID getUUIDValue() { - final String stringValue = currentNode.getAsString(); - if (stringValue == null) return null; - return UUID.fromString(stringValue); + return gson.fromJson(currentNode, UUID.class); } @Nullable public OffsetDateTime getOffsetDateTimeValue() { - final String stringValue = currentNode.getAsString(); - if (stringValue == null) return null; - 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; - } - } + return gson.fromJson(currentNode, OffsetDateTime.class); } @Nullable public LocalDate getLocalDateValue() { - final String stringValue = currentNode.getAsString(); - if (stringValue == null) return null; - return LocalDate.parse(stringValue); + return gson.fromJson(currentNode, LocalDate.class); } @Nullable public LocalTime getLocalTimeValue() { - final String stringValue = currentNode.getAsString(); - if (stringValue == null) return null; - return LocalTime.parse(stringValue); + return gson.fromJson(currentNode, LocalTime.class); } @Nullable public PeriodAndDuration getPeriodAndDurationValue() { - final String stringValue = currentNode.getAsString(); - if (stringValue == null) return null; - return PeriodAndDuration.parse(stringValue); + return gson.fromJson(currentNode, PeriodAndDuration.class); } @Nullable private T getPrimitiveValue( @Nonnull final Class targetClass, @Nonnull final JsonParseNode itemNode) { - if (targetClass == Boolean.class) { - return (T) itemNode.getBooleanValue(); - } else if (targetClass == Short.class) { - return (T) itemNode.getShortValue(); - } else if (targetClass == Byte.class) { - return (T) itemNode.getByteValue(); - } else if (targetClass == BigDecimal.class) { - return (T) itemNode.getBigDecimalValue(); - } else if (targetClass == String.class) { - return (T) itemNode.getStringValue(); - } else if (targetClass == Integer.class) { - return (T) itemNode.getIntegerValue(); - } else if (targetClass == Float.class) { - return (T) itemNode.getFloatValue(); - } else if (targetClass == Double.class) { - return (T) itemNode.getDoubleValue(); - } else if (targetClass == Long.class) { - return (T) itemNode.getLongValue(); - } else if (targetClass == UUID.class) { - return (T) itemNode.getUUIDValue(); - } else if (targetClass == OffsetDateTime.class) { - return (T) itemNode.getOffsetDateTimeValue(); - } else if (targetClass == LocalDate.class) { - return (T) itemNode.getLocalDateValue(); - } else if (targetClass == LocalTime.class) { - return (T) itemNode.getLocalTimeValue(); - } else if (targetClass == PeriodAndDuration.class) { - return (T) itemNode.getPeriodAndDurationValue(); - } else { - throw new RuntimeException("unknown type to deserialize " + targetClass.getName()); - } + return gson.fromJson(itemNode.currentNode, targetClass); } private List iterateOnArray(JsonElement jsonElement, Function fn) { @@ -171,7 +138,7 @@ private List iterateOnArray(JsonElement jsonElement, Function result = new ArrayList<>(); while (sourceIterator.hasNext()) { final JsonElement item = sourceIterator.next(); - final JsonParseNode itemNode = new JsonParseNode(item); + final JsonParseNode itemNode = createNewNode(item); itemNode.setOnBeforeAssignFieldValues(this.getOnBeforeAssignFieldValues()); itemNode.setOnAfterAssignFieldValues(this.getOnAfterAssignFieldValues()); result.add(fn.apply(itemNode)); @@ -237,7 +204,7 @@ else if (element.isJsonPrimitive()) { element.getAsJsonObject().entrySet()) { final String fieldKey = fieldEntry.getKey(); final JsonElement fieldValue = fieldEntry.getValue(); - final JsonParseNode childNode = new JsonParseNode(fieldValue); + final JsonParseNode childNode = createNewNode(fieldValue); childNode.setOnBeforeAssignFieldValues(this.getOnBeforeAssignFieldValues()); childNode.setOnAfterAssignFieldValues(this.getOnAfterAssignFieldValues()); propertiesMap.put(fieldKey, childNode.getUntypedValue()); @@ -294,7 +261,7 @@ private void assignFieldValues( final JsonElement fieldValue = fieldEntry.getValue(); if (fieldValue.isJsonNull()) continue; if (fieldDeserializer != null) { - final JsonParseNode itemNode = new JsonParseNode(fieldValue); + final JsonParseNode itemNode = createNewNode(fieldValue); itemNode.setOnBeforeAssignFieldValues(this.onBeforeAssignFieldValues); itemNode.setOnAfterAssignFieldValues(this.onAfterAssignFieldValues); fieldDeserializer.accept(itemNode); @@ -344,10 +311,6 @@ public void setOnAfterAssignFieldValues(@Nullable final Consumer value } @Nullable public byte[] getByteArrayValue() { - final String base64 = this.getStringValue(); - if (base64 == null || base64.isEmpty()) { - return null; - } - return Base64.getDecoder().decode(base64); + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, byte[].class) : null; } } diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNodeFactory.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNodeFactory.java index c0c68eaa..65a8d487 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNodeFactory.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNodeFactory.java @@ -1,5 +1,6 @@ package com.microsoft.kiota.serialization; +import com.google.gson.Gson; import com.google.gson.JsonParser; import jakarta.annotation.Nonnull; @@ -12,8 +13,21 @@ /** Creates new Json parse nodes from the payload. */ public class JsonParseNodeFactory implements ParseNodeFactory { + private final Gson gson; + /** Creates a new factory */ - public JsonParseNodeFactory() {} + public JsonParseNodeFactory() { + this(DefaultGsonBuilder.getDefaultInstance()); + } + + /** + * Creates a new factory + * @param gson the {@link Gson} instance to use for parsing value types. + */ + public JsonParseNodeFactory(@Nonnull Gson gson) { + Objects.requireNonNull(gson, "parameter gson cannot be null"); + this.gson = gson; + } /** {@inheritDoc} */ @Nonnull public String getValidContentType() { @@ -35,7 +49,7 @@ public JsonParseNodeFactory() {} } try (final InputStreamReader reader = new InputStreamReader(rawResponse, StandardCharsets.UTF_8)) { - return new JsonParseNode(JsonParser.parseReader(reader)); + return new JsonParseNode(JsonParser.parseReader(reader), gson); } catch (IOException ex) { throw new RuntimeException("could not close the reader", ex); } diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java index f050d996..11ca51d7 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java @@ -1,5 +1,6 @@ package com.microsoft.kiota.serialization; +import com.google.gson.Gson; import com.google.gson.stream.JsonWriter; import com.microsoft.kiota.PeriodAndDuration; @@ -17,8 +18,6 @@ import java.time.LocalDate; import java.time.LocalTime; import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Base64; import java.util.EnumSet; import java.util.List; import java.util.Map; @@ -34,10 +33,22 @@ public class JsonSerializationWriter implements SerializationWriter { private final ByteArrayOutputStream stream = new ByteArrayOutputStream(); private final JsonWriter writer; + private final Gson gson; - /** Creates a new instance of a json serialization writer */ + /** + * Creates a new instance of a json serialization writer + */ public JsonSerializationWriter() { + this(DefaultGsonBuilder.getDefaultInstance()); + } + + /** + * Creates a new instance of a json serialization writer + * @param gson the {@link Gson} instance to use for writing value types. + */ + public JsonSerializationWriter(@Nonnull Gson gson) { this.writer = new JsonWriter(new OutputStreamWriter(this.stream, StandardCharsets.UTF_8)); + this.gson = Objects.requireNonNull(gson, "parameter gson cannot be null"); } public void writeStringValue(@Nullable final String key, @Nullable final String value) { @@ -46,7 +57,7 @@ public void writeStringValue(@Nullable final String key, @Nullable final String if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(String.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -58,7 +69,7 @@ public void writeBooleanValue(@Nullable final String key, @Nullable final Boolea if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(Boolean.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -70,7 +81,7 @@ public void writeShortValue(@Nullable final String key, @Nullable final Short va if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(Short.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -82,7 +93,7 @@ public void writeByteValue(@Nullable final String key, @Nullable final Byte valu if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(Byte.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -94,7 +105,7 @@ public void writeBigDecimalValue(@Nullable final String key, @Nullable final Big if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(BigDecimal.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -106,7 +117,7 @@ public void writeIntegerValue(@Nullable final String key, @Nullable final Intege if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(Integer.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -118,7 +129,7 @@ public void writeFloatValue(@Nullable final String key, @Nullable final Float va if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(Float.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -130,7 +141,7 @@ public void writeDoubleValue(@Nullable final String key, @Nullable final Double if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(Double.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -142,7 +153,7 @@ public void writeLongValue(@Nullable final String key, @Nullable final Long valu if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(Long.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -154,7 +165,7 @@ public void writeUUIDValue(@Nullable final String key, @Nullable final UUID valu if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value.toString()); + gson.getAdapter(UUID.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -167,7 +178,7 @@ public void writeOffsetDateTimeValue( if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)); + gson.getAdapter(OffsetDateTime.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -179,7 +190,7 @@ public void writeLocalDateValue(@Nullable final String key, @Nullable final Loca if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value.format(DateTimeFormatter.ISO_LOCAL_DATE)); + gson.getAdapter(LocalDate.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -191,7 +202,7 @@ public void writeLocalTimeValue(@Nullable final String key, @Nullable final Loca if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value.format(DateTimeFormatter.ISO_LOCAL_TIME)); + gson.getAdapter(LocalTime.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -204,7 +215,7 @@ public void writePeriodAndDurationValue( if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value.toString()); + gson.getAdapter(PeriodAndDuration.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -528,6 +539,14 @@ public void setOnStartObjectSerialization( } public void writeByteArrayValue(@Nullable final String key, @Nullable final byte[] value) { - if (value != null) this.writeStringValue(key, Base64.getEncoder().encodeToString(value)); + if (value != null) + try { + if (key != null && !key.isEmpty()) { + writer.name(key); + } + gson.getAdapter(byte[].class).write(writer, value); + } catch (IOException ex) { + throw new RuntimeException("could not serialize value", ex); + } } } diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriterFactory.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriterFactory.java index c72d01d0..f6420a1c 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriterFactory.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriterFactory.java @@ -1,13 +1,28 @@ package com.microsoft.kiota.serialization; +import com.google.gson.Gson; + import jakarta.annotation.Nonnull; import java.util.Objects; /** Creates new Json serialization writers. */ public class JsonSerializationWriterFactory implements SerializationWriterFactory { + private final Gson gson; + /** Creates a new factory */ - public JsonSerializationWriterFactory() {} + public JsonSerializationWriterFactory() { + this(DefaultGsonBuilder.getDefaultInstance()); + } + + /** + * Creates a new factory + * @param gson the {@link Gson} instance to use for writing value types. + */ + public JsonSerializationWriterFactory(@Nonnull Gson gson) { + Objects.requireNonNull(gson, "gson contentType cannot be null"); + this.gson = gson; + } /** {@inheritDoc} */ @Nonnull public String getValidContentType() { @@ -25,6 +40,6 @@ public JsonSerializationWriterFactory() {} } else if (!contentType.equals(validContentType)) { throw new IllegalArgumentException("expected a " + validContentType + " content type"); } - return new JsonSerializationWriter(); + return new JsonSerializationWriter(gson); } } 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 ec31bd0e..8447f083 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,8 +1,17 @@ package com.microsoft.kiota.serialization; -import static org.junit.jupiter.api.Assertions.*; +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.assertTrue; +import com.google.gson.Gson; import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; import com.microsoft.kiota.serialization.mocks.MyEnum; import com.microsoft.kiota.serialization.mocks.TestEntity; import com.microsoft.kiota.serialization.mocks.UntypedTestEntity; @@ -12,7 +21,11 @@ import org.junit.jupiter.params.provider.ValueSource; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeParseException; class JsonParseNodeTests { @@ -66,6 +79,44 @@ class JsonParseNodeTests { + " }\r\n" + "}"; + public static final DateTimeFormatter customFormatter = + new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + .parseLenient() + .appendOffset("+HHmm", "+0000") + .parseStrict() + .toFormatter(); + + public static final Gson customGson = + DefaultGsonBuilder.getDefaultBuilder() + .registerTypeAdapter( + OffsetDateTime.class, + new TypeAdapter() { + @Override + public OffsetDateTime read(JsonReader in) throws IOException { + String stringValue = in.nextString(); + try { + return customFormatter.parse( + stringValue, OffsetDateTime::from); + } catch (DateTimeParseException ex) { + throw new JsonSyntaxException( + "Failed parsing '" + + stringValue + + "' as LocalDate; at path " + + in.getPreviousPath(), + ex); + } + } + + @Override + public void write(JsonWriter out, OffsetDateTime value) + throws IOException { + out.value(customFormatter.format(value)); + } + }.nullSafe()) + .create(); + @Test void itDDoesNotFailForGetChildElementOnMissingKey() throws UnsupportedEncodingException { final var initialString = "{displayName\": \"Microsoft Teams Meeting\"}"; @@ -79,7 +130,9 @@ void itDDoesNotFailForGetChildElementOnMissingKey() throws UnsupportedEncodingEx 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(); + final var result = + new JsonParseNode(jsonElement, DefaultGsonBuilder.getDefaultInstance()) + .getOffsetDateTimeValue(); assertEquals(dateTimeOffsetString, result.toString()); } @@ -87,7 +140,9 @@ void testParsesDateTimeOffset() { void testParsesDateTimeStringWithoutOffsetToDateTimeOffset() { final var dateTimeString = "2024-02-12T19:47:39"; final var jsonElement = JsonParser.parseString("\"" + dateTimeString + "\""); - final var result = new JsonParseNode(jsonElement).getOffsetDateTimeValue(); + final var result = + new JsonParseNode(jsonElement, DefaultGsonBuilder.getDefaultInstance()) + .getOffsetDateTimeValue(); assertEquals(dateTimeString + "Z", result.toString()); } @@ -96,13 +151,23 @@ void testParsesDateTimeStringWithoutOffsetToDateTimeOffset() { void testInvalidOffsetDateTimeStringThrowsException(final String dateTimeString) { final var jsonElement = JsonParser.parseString("\"" + dateTimeString + "\""); try { - new JsonParseNode(jsonElement).getOffsetDateTimeValue(); + new JsonParseNode(jsonElement, DefaultGsonBuilder.getDefaultInstance()) + .getOffsetDateTimeValue(); } catch (final Exception ex) { - assertInstanceOf(DateTimeParseException.class, ex); + assertInstanceOf(JsonSyntaxException.class, ex); assertTrue(ex.getMessage().contains(dateTimeString)); } } + @Test + void testNonStandardOffsetDateTimeParsing() { + final var dateTimeString = "2024-02-12T19:47:39+0000"; + final var jsonElement = JsonParser.parseString("\"" + dateTimeString + "\""); + final var parsedOffsetDateTime = + new JsonParseNode(jsonElement, customGson).getOffsetDateTimeValue(); + assertEquals(OffsetDateTime.parse("2024-02-12T19:47:39+00:00"), parsedOffsetDateTime); + } + @Test void getEntityWithArrayInAdditionalData() throws UnsupportedEncodingException { final var rawResponse = new ByteArrayInputStream(testJsonString.getBytes("UTF-8")); diff --git a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonSerializationWriterTests.java b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonSerializationWriterTests.java index f9a5f503..9bdac251 100644 --- a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonSerializationWriterTests.java +++ b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonSerializationWriterTests.java @@ -1,8 +1,11 @@ package com.microsoft.kiota.serialization; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import com.microsoft.kiota.Compatibility; +import com.microsoft.kiota.PeriodAndDuration; import com.microsoft.kiota.serialization.mocks.MyEnum; import com.microsoft.kiota.serialization.mocks.TestEntity; import com.microsoft.kiota.serialization.mocks.UntypedTestEntity; @@ -10,10 +13,17 @@ import org.junit.jupiter.api.Test; import java.io.IOException; +import java.math.BigDecimal; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.Period; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.UUID; class JsonSerializationWriterTests { @@ -53,7 +63,8 @@ void writesSampleObjectValueWithPrimitivesInAdditionalData() throws IOException .getAdditionalData() .put("aliases", aliases); // place a collection in the additional data - try (final var jsonSerializer = new JsonSerializationWriter()) { + try (final var jsonSerializer = + new JsonSerializationWriter(DefaultGsonBuilder.getDefaultInstance())) { jsonSerializer.writeObjectValue("", testEntity); var contentStream = jsonSerializer.getSerializedContent(); var serializedJsonString = @@ -66,6 +77,22 @@ void writesSampleObjectValueWithPrimitivesInAdditionalData() throws IOException } } + @Test + void useNonStandardOffsetDateTimeFormat() throws IOException { + var testEntity = new TestEntity(); + testEntity.setCreatedDateTime(OffsetDateTime.parse("2024-02-12T19:47:39+00:00")); + try (final var jsonSerializer = + new JsonSerializationWriter(JsonParseNodeTests.customGson)) { + jsonSerializer.writeObjectValue("", testEntity); + var contentStream = jsonSerializer.getSerializedContent(); + var serializedJsonString = + new String(Compatibility.readAllBytes(contentStream), "UTF-8"); + // Assert + var expectedString = "{\"createdDateTime\":\"2024-02-12T19:47:39+0000\"}"; + assertEquals(expectedString, serializedJsonString); + } + } + @Test void writesSampleObjectValueWithParsableInAdditionalData() throws IOException { var testEntity = new TestEntity(); @@ -81,7 +108,8 @@ void writesSampleObjectValueWithParsableInAdditionalData() throws IOException { .getAdditionalData() .put("manager", managerAdditionalData); // place a parsable in the addtionaldata - try (final var jsonSerializer = new JsonSerializationWriter()) { + try (final var jsonSerializer = + new JsonSerializationWriter(DefaultGsonBuilder.getDefaultInstance())) { jsonSerializer.writeObjectValue("", testEntity); var contentStream = jsonSerializer.getSerializedContent(); var serializedJsonString = @@ -186,7 +214,8 @@ void writesSampleObjectValueWithUntypedProperties() throws IOException { } })); - try (final var jsonSerializer = new JsonSerializationWriter()) { + try (final var jsonSerializer = + new JsonSerializationWriter(DefaultGsonBuilder.getDefaultInstance())) { jsonSerializer.writeObjectValue("", untypedTestEntity); var contentStream = jsonSerializer.getSerializedContent(); var serializedJsonString = @@ -205,4 +234,66 @@ void writesSampleObjectValueWithUntypedProperties() throws IOException { assertEquals(expectedString, serializedJsonString); } } + + @Test + void parseWrittenValues() throws IOException { + writeAndParse( + ParseNode::getStringValue, SerializationWriter::writeStringValue, "just a string"); + writeAndParse(ParseNode::getBooleanValue, SerializationWriter::writeBooleanValue, true); + writeAndParse(ParseNode::getByteValue, SerializationWriter::writeByteValue, (byte) 3); + writeAndParse(ParseNode::getShortValue, SerializationWriter::writeShortValue, (short) 42); + writeAndParse( + ParseNode::getBigDecimalValue, + SerializationWriter::writeBigDecimalValue, + new BigDecimal(123456789L)); + writeAndParse(ParseNode::getIntegerValue, SerializationWriter::writeIntegerValue, 54321); + writeAndParse( + ParseNode::getFloatValue, SerializationWriter::writeFloatValue, (float) 67.89); + writeAndParse(ParseNode::getDoubleValue, SerializationWriter::writeDoubleValue, 3245.12356); + writeAndParse(ParseNode::getLongValue, SerializationWriter::writeLongValue, -543219876L); + writeAndParse( + ParseNode::getUUIDValue, SerializationWriter::writeUUIDValue, UUID.randomUUID()); + writeAndParse( + ParseNode::getOffsetDateTimeValue, + SerializationWriter::writeOffsetDateTimeValue, + OffsetDateTime.now()); + writeAndParse( + ParseNode::getLocalDateValue, + SerializationWriter::writeLocalDateValue, + LocalDate.now()); + writeAndParse( + ParseNode::getLocalTimeValue, + SerializationWriter::writeLocalTimeValue, + LocalTime.now()); + writeAndParse( + ParseNode::getPeriodAndDurationValue, + SerializationWriter::writePeriodAndDurationValue, + PeriodAndDuration.of(Period.ofYears(3), Duration.ofHours(6))); + writeAndParse( + ParseNode::getByteArrayValue, + SerializationWriter::writeByteArrayValue, + new byte[] {8, 10, 127, -50, 0}); + } + + private void writeAndParse( + TestParsable.ParseMethod parseMethod, + TestParsable.WriteMethod writeMethod, + T value) + throws IOException { + var testParsable = new TestParsable<>(parseMethod, writeMethod, value); + var writer = new JsonSerializationWriter(DefaultGsonBuilder.getDefaultInstance()); + writer.writeObjectValue(null, testParsable); + + var parseNodeFactory = new JsonParseNodeFactory(DefaultGsonBuilder.getDefaultInstance()); + var parseNode = + parseNodeFactory.getParseNode("application/json", writer.getSerializedContent()); + var result = parseNode.getObjectValue(TestParsable.factory(parseMethod, writeMethod)); + if (value instanceof byte[] bytea) { + assertArrayEquals(bytea, (byte[]) result.getRealValue()); + } else { + assertEquals(value, result.getRealValue()); + } + assertNull(result.getNullValue()); + writer.close(); + } } diff --git a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/TestParsable.java b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/TestParsable.java new file mode 100644 index 00000000..c0dd35fb --- /dev/null +++ b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/TestParsable.java @@ -0,0 +1,71 @@ +package com.microsoft.kiota.serialization; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +public class TestParsable implements Parsable { + @FunctionalInterface + public interface WriteMethod { + public void write(SerializationWriter writer, String key, V value); + } + + @FunctionalInterface + public interface ParseMethod { + public V parse(ParseNode parseNode); + } + + private T realValue; + + private T nullValue; + + private final ParseMethod parseMethod; + + private final WriteMethod writeMethod; + + public TestParsable(ParseMethod parseMethod, WriteMethod writeMethod) { + this(parseMethod, writeMethod, null); + } + + public TestParsable(ParseMethod parseMethod, WriteMethod writeMethod, T value) { + this.parseMethod = parseMethod; + this.writeMethod = writeMethod; + this.realValue = value; + } + + @Override + public Map> getFieldDeserializers() { + final HashMap> deserializerMap = + new HashMap>(2); + deserializerMap.put( + "realValue", + (n) -> { + this.realValue = parseMethod.parse(n); + }); + deserializerMap.put( + "nullValue", + (n) -> { + this.nullValue = parseMethod.parse(n); + }); + return deserializerMap; + } + + @Override + public void serialize(SerializationWriter writer) { + writeMethod.write(writer, "realValue", realValue); + writeMethod.write(writer, "nullValue", nullValue); + } + + public T getRealValue() { + return realValue; + } + + public T getNullValue() { + return nullValue; + } + + public static ParsableFactory> factory( + ParseMethod parseMethod, WriteMethod writeMethod) { + return (n) -> new TestParsable(parseMethod, writeMethod); + } +}