Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow backing store serialization configuration in serialization helpers #1608

Merged
merged 12 commits into from
Oct 8, 2024
1 change: 1 addition & 0 deletions components/abstractions/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ android {
disable "GradleDependency"
disable "NewerVersionAvailable"
disable "DuplicatePlatformClasses" // xpp3 added by azure-identity
disable "LambdaLast" // Wrongly enforced in KiotaJsonSerialization helpers
}
sourceSets {
main {
Expand Down
1 change: 1 addition & 0 deletions components/abstractions/gradle/dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.1'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.1'
testImplementation 'org.mockito:mockito-core:5.14.1'
testImplementation project(':components:serialization:json')

// Use JUnit Jupiter Engine for testing.
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
Expand Down
6 changes: 2 additions & 4 deletions components/abstractions/spotBugsExcludeFilter.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,11 @@ xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubu
<Bug pattern="RV_EXCEPTION_NOT_THROWN"/>
<Class name="com.microsoft.kiota.serialization.SerializationHelpersTest" />
</Match>
<Match>
<Bug pattern="EI_EXPOSE_REP" />
<Class name="com.microsoft.kiota.serialization.mocks.TestEntity" />
</Match>
<Match>
<Bug pattern="EI_EXPOSE_REP" />
<Or>
<Class name="com.microsoft.kiota.serialization.mocks.TestEntity" />
<Class name="com.microsoft.kiota.serialization.mocks.TestBackedModelEntity" />
<Class name="com.microsoft.kiota.TestEntity" />
<Class name="com.microsoft.kiota.BaseCollectionPaginationCountResponse" />
</Or>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ private KiotaJsonSerialization() {}
return KiotaSerialization.serializeAsStream(CONTENT_TYPE, value);
}

/**
* Serializes the given value to a stream
* @param <T> the type of the value to serialize
* @param value the value to serialize
* @param serializeOnlyChangedValues whether to serialize all values in value if value is a BackedModel
* @return the serialized value as a stream
* @throws IOException when the stream cannot be closed or read.
*/
@Nonnull public static <T extends Parsable> InputStream serializeAsStream(
@Nonnull final T value, final boolean serializeOnlyChangedValues) throws IOException {
return KiotaSerialization.serializeAsStream(
CONTENT_TYPE, value, serializeOnlyChangedValues);
}

/**
* Serializes the given value to a string
* @param <T> the type of the value to serialize
Expand All @@ -38,6 +52,20 @@ private KiotaJsonSerialization() {}
return KiotaSerialization.serializeAsString(CONTENT_TYPE, value);
}

/**
* Serializes the given value to a string
* @param <T> the type of the value to serialize
* @param value the value to serialize
* @param serializeOnlyChangedValues whether to serialize all values in value if value is a BackedModel
* @return the serialized value as a string
* @throws IOException when the stream cannot be closed or read.
*/
@Nonnull public static <T extends Parsable> String serializeAsString(
@Nonnull final T value, final boolean serializeOnlyChangedValues) throws IOException {
return KiotaSerialization.serializeAsString(
CONTENT_TYPE, value, serializeOnlyChangedValues);
}

/**
* Serializes the given value to a stream
* @param <T> the type of the value to serialize
Expand All @@ -50,6 +78,21 @@ private KiotaJsonSerialization() {}
return KiotaSerialization.serializeAsStream(CONTENT_TYPE, values);
}

/**
* Serializes the given value to a stream
* @param <T> the type of the value to serialize
* @param values the values to serialize
* @param serializeOnlyChangedValues whether to serialize all values in value if value is a BackedModel
* @return the serialized value as a stream
* @throws IOException when the stream cannot be closed or read.
*/
@Nonnull public static <T extends Parsable> InputStream serializeAsStream(
@Nonnull final Iterable<T> values, final boolean serializeOnlyChangedValues)
throws IOException {
return KiotaSerialization.serializeAsStream(
CONTENT_TYPE, values, serializeOnlyChangedValues);
}

/**
* Serializes the given value to a string
* @param <T> the type of the value to serialize
Expand All @@ -62,6 +105,21 @@ private KiotaJsonSerialization() {}
return KiotaSerialization.serializeAsString(CONTENT_TYPE, values);
}

/**
* Serializes the given value to a string
* @param <T> the type of the value to serialize
* @param values the values to serialize
* @param serializeOnlyChangedValues whether to serialize all values in value if value is a BackedModel
* @return the serialized value as a string
* @throws IOException when the stream cannot be closed or read.
*/
@Nonnull public static <T extends Parsable> String serializeAsString(
@Nonnull final Iterable<T> values, final boolean serializeOnlyChangedValues)
throws IOException {
return KiotaSerialization.serializeAsString(
CONTENT_TYPE, values, serializeOnlyChangedValues);
}

/**
* Deserializes the given stream to a model object
* @param <T> the type of the value to deserialize
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
public final class KiotaSerialization {
private static final String CHARSET_NAME = "UTF-8";
private static final boolean DEFAULT_SERIALIZE_ONLY_CHANGED_VALUES = true;

private KiotaSerialization() {}

Expand All @@ -29,7 +30,26 @@ private KiotaSerialization() {}
*/
@Nonnull public static <T extends Parsable> InputStream serializeAsStream(
@Nonnull final String contentType, @Nonnull final T value) throws IOException {
try (final SerializationWriter writer = getSerializationWriter(contentType, value)) {
return serializeAsStream(contentType, value, DEFAULT_SERIALIZE_ONLY_CHANGED_VALUES);
}

/**
* Serializes the given value to a stream and configures returned values by the backing store if available
* @param <T> the type of the value to serialize
* @param contentType the content type to use for serialization
* @param value the value to serialize
* @param serializeOnlyChangedValues whether to serialize all values in value if value is a BackedModel
* @return the serialized value as a stream
* @throws IOException when the stream cannot be closed or read.
*/
@Nonnull public static <T extends Parsable> InputStream serializeAsStream(
@Nonnull final String contentType,
@Nonnull final T value,
final boolean serializeOnlyChangedValues)
throws IOException {
Objects.requireNonNull(value);
try (final SerializationWriter writer =
getSerializationWriter(contentType, serializeOnlyChangedValues)) {
writer.writeObjectValue("", value);
return writer.getSerializedContent();
}
Expand All @@ -45,7 +65,27 @@ private KiotaSerialization() {}
*/
@Nonnull public static <T extends Parsable> String serializeAsString(
@Nonnull final String contentType, @Nonnull final T value) throws IOException {
try (final InputStream stream = serializeAsStream(contentType, value)) {
Objects.requireNonNull(value);
return serializeAsString(contentType, value, DEFAULT_SERIALIZE_ONLY_CHANGED_VALUES);
}

/**
* Serializes the given value to a string
* @param <T> the type of the value to serialize
* @param contentType the content type to use for serialization
* @param value the value to serialize
* @param serializeOnlyChangedValues whether to serialize all values in value if value is a BackedModel
* @return the serialized value as a string
* @throws IOException when the stream cannot be closed or read.
*/
@Nonnull public static <T extends Parsable> String serializeAsString(
@Nonnull final String contentType,
@Nonnull final T value,
final boolean serializeOnlyChangedValues)
throws IOException {
Objects.requireNonNull(value);
try (final InputStream stream =
serializeAsStream(contentType, value, serializeOnlyChangedValues)) {
return new String(Compatibility.readAllBytes(stream), CHARSET_NAME);
}
}
Expand All @@ -61,7 +101,27 @@ private KiotaSerialization() {}
@Nonnull public static <T extends Parsable> InputStream serializeAsStream(
@Nonnull final String contentType, @Nonnull final Iterable<T> values)
throws IOException {
try (final SerializationWriter writer = getSerializationWriter(contentType, values)) {
Objects.requireNonNull(values);
return serializeAsStream(contentType, values, DEFAULT_SERIALIZE_ONLY_CHANGED_VALUES);
}

/**
* Serializes the given value to a stream
* @param <T> the type of the value to serialize
* @param contentType the content type to use for serialization
* @param values the values to serialize
* @param serializeOnlyChangedValues whether to serialize all values in value if value is a BackedModel
* @return the serialized value as a stream
* @throws IOException when the stream cannot be closed or read.
*/
@Nonnull public static <T extends Parsable> InputStream serializeAsStream(
@Nonnull final String contentType,
@Nonnull final Iterable<T> values,
final boolean serializeOnlyChangedValues)
throws IOException {
Objects.requireNonNull(values);
try (final SerializationWriter writer =
getSerializationWriter(contentType, serializeOnlyChangedValues)) {
writer.writeCollectionOfObjectValues("", values);
return writer.getSerializedContent();
}
Expand All @@ -78,20 +138,39 @@ private KiotaSerialization() {}
@Nonnull public static <T extends Parsable> String serializeAsString(
@Nonnull final String contentType, @Nonnull final Iterable<T> values)
throws IOException {
try (final InputStream stream = serializeAsStream(contentType, values)) {
Objects.requireNonNull(values);
return serializeAsString(contentType, values, DEFAULT_SERIALIZE_ONLY_CHANGED_VALUES);
}

/**
* Serializes the given value to a string
* @param <T> the type of the value to serialize
* @param contentType the content type to use for serialization
* @param values the values to serialize
* @param serializeOnlyChangedValues whether to serialize all values in value if value is a BackedModel
* @return the serialized value as a string
* @throws IOException when the stream cannot be closed or read.
*/
@Nonnull public static <T extends Parsable> String serializeAsString(
@Nonnull final String contentType,
@Nonnull final Iterable<T> values,
final boolean serializeOnlyChangedValues)
throws IOException {
Objects.requireNonNull(values);
try (final InputStream stream =
serializeAsStream(contentType, values, serializeOnlyChangedValues)) {
return new String(Compatibility.readAllBytes(stream), CHARSET_NAME);
}
}

private static SerializationWriter getSerializationWriter(
@Nonnull final String contentType, @Nonnull final Object value) {
@Nonnull final String contentType, final boolean serializeOnlyChangedValues) {
Objects.requireNonNull(contentType);
Objects.requireNonNull(value);
if (contentType.isEmpty()) {
throw new NullPointerException("content type cannot be empty");
}
return SerializationWriterFactoryRegistry.defaultInstance.getSerializationWriter(
contentType);
contentType, serializeOnlyChangedValues);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.microsoft.kiota.serialization;

import com.microsoft.kiota.store.BackingStoreSerializationWriterProxyFactory;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;

import java.util.HashMap;
import java.util.Objects;
Expand Down Expand Up @@ -32,24 +35,77 @@ public SerializationWriterFactoryRegistry() {

@Override
@Nonnull public SerializationWriter getSerializationWriter(@Nonnull final String contentType) {
return getSerializationWriter(contentType, true);
}

/**
* Get a Serialization Writer with backing store configured with serializeOnlyChangedValues
* @param contentType
* @param serializeOnlyChangedValues control backing store functionality
* @return the serialization writer
* @throws RuntimeException when no factory is found for content type
*/
@Nonnull public SerializationWriter getSerializationWriter(
@Nonnull final String contentType, final boolean serializeOnlyChangedValues) {
Objects.requireNonNull(contentType, "parameter contentType cannot be null");
if (contentType.isEmpty()) {
throw new NullPointerException("contentType cannot be empty");
}
final String vendorSpecificContentType = contentType.split(";")[0];
if (contentTypeAssociatedFactories.containsKey(vendorSpecificContentType)) {
return contentTypeAssociatedFactories
.get(vendorSpecificContentType)
.getSerializationWriter(vendorSpecificContentType);
String cleanedContentType = getVendorSpecificContentType(contentType);
SerializationWriterFactory factory = getSerializationWriterFactory(cleanedContentType);
if (factory == null) {
cleanedContentType = getCleanedVendorSpecificContentType(cleanedContentType);
factory =
getSerializationWriterFactory(
getCleanedVendorSpecificContentType(cleanedContentType));
if (factory == null) {
throw new RuntimeException(
"Content type "
+ contentType
+ " does not have a factory to be serialized");
}
}
final String cleanedContentType =
contentTypeVendorCleanupPattern.matcher(vendorSpecificContentType).replaceAll("");
if (contentTypeAssociatedFactories.containsKey(cleanedContentType)) {
return contentTypeAssociatedFactories
.get(cleanedContentType)
.getSerializationWriter(cleanedContentType);
if (!serializeOnlyChangedValues) {
if (factory instanceof BackingStoreSerializationWriterProxyFactory) {
return ((BackingStoreSerializationWriterProxyFactory) factory)
.getSerializationWriter(cleanedContentType, serializeOnlyChangedValues);
}
}
return factory.getSerializationWriter(cleanedContentType);
}

/**
* Gets a SerializationWriterFactory that is mapped to a cleaned content type string
* @param contentType wrapper object carrying initial content type and result of parsing it
* @return the serialization writer factory or null if no mapped factory is found
*/
@Nullable private SerializationWriterFactory getSerializationWriterFactory(
@Nonnull final String contentType) {
if (contentTypeAssociatedFactories.containsKey(contentType)) {
return contentTypeAssociatedFactories.get(contentType);
}
throw new RuntimeException(
"Content type " + contentType + " does not have a factory to be serialized");
return null;
}

/**
* Splits content type by ; and returns first segment or original contentType
* @param contentType
* @return vendor specific content type
*/
@Nonnull private String getVendorSpecificContentType(@Nonnull final String contentType) {
String[] split = contentType.split(";");
if (split.length >= 1) {
return split[0];
}
return contentType;
}

/**
* Does a regex match on the content type replacing special characters
* @param contentType
* @return cleaned content type
*/
@Nonnull private String getCleanedVendorSpecificContentType(@Nonnull final String contentType) {
return contentTypeVendorCleanupPattern.matcher(contentType).replaceAll("");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
/** Proxy factory that allows the composition of before and after callbacks on existing factories. */
public abstract class SerializationWriterProxyFactory implements SerializationWriterFactory {
@Nonnull public String getValidContentType() {
return _concrete.getValidContentType();
return proxiedFactory.getValidContentType();
}

private final SerializationWriterFactory _concrete;
protected final SerializationWriterFactory proxiedFactory;
private final Consumer<Parsable> _onBefore;
private final Consumer<Parsable> _onAfter;
private final BiConsumer<Parsable, SerializationWriter> _onStart;
Expand All @@ -30,14 +30,14 @@ public SerializationWriterProxyFactory(
@Nullable final Consumer<Parsable> onBeforeSerialization,
@Nullable final Consumer<Parsable> onAfterSerialization,
@Nullable final BiConsumer<Parsable, SerializationWriter> onStartObjectSerialization) {
_concrete = Objects.requireNonNull(concrete);
proxiedFactory = Objects.requireNonNull(concrete);
_onBefore = onBeforeSerialization;
_onAfter = onAfterSerialization;
_onStart = onStartObjectSerialization;
}

@Nonnull public SerializationWriter getSerializationWriter(@Nonnull final String contentType) {
final SerializationWriter writer = _concrete.getSerializationWriter(contentType);
final SerializationWriter writer = proxiedFactory.getSerializationWriter(contentType);
final Consumer<Parsable> originalBefore = writer.getOnBeforeObjectSerialization();
final Consumer<Parsable> originalAfter = writer.getOnAfterObjectSerialization();
final BiConsumer<Parsable, SerializationWriter> originalStart =
Expand Down
Loading
Loading