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/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(
final boolean serializeOnlyChangedValues, @Nonnull final T value) throws IOException {
return KiotaSerialization.serializeAsStream(
CONTENT_TYPE, serializeOnlyChangedValues, value);
}

/**
* 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(
final boolean serializeOnlyChangedValues, @Nonnull final T value) throws IOException {
return KiotaSerialization.serializeAsString(
CONTENT_TYPE, serializeOnlyChangedValues, value);
}

/**
* 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(
final boolean serializeOnlyChangedValues, @Nonnull final Iterable<T> values)
throws IOException {
return KiotaSerialization.serializeAsStream(
CONTENT_TYPE, serializeOnlyChangedValues, values);
}

/**
* 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(
final boolean serializeOnlyChangedValues, @Nonnull final Iterable<T> values)
throws IOException {
return KiotaSerialization.serializeAsString(
CONTENT_TYPE, serializeOnlyChangedValues, values);
}

/**
* 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, DEFAULT_SERIALIZE_ONLY_CHANGED_VALUES, value);
}

/**
* 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,
final boolean serializeOnlyChangedValues,
Ndiritu marked this conversation as resolved.
Show resolved Hide resolved
@Nonnull final T value)
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, DEFAULT_SERIALIZE_ONLY_CHANGED_VALUES, value);
}

/**
* 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,
final boolean serializeOnlyChangedValues,
@Nonnull final T value)
throws IOException {
Objects.requireNonNull(value);
try (final InputStream stream =
serializeAsStream(contentType, serializeOnlyChangedValues, value)) {
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, DEFAULT_SERIALIZE_ONLY_CHANGED_VALUES, 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,
final boolean serializeOnlyChangedValues,
@Nonnull final Iterable<T> values)
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, DEFAULT_SERIALIZE_ONLY_CHANGED_VALUES, 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,
final boolean serializeOnlyChangedValues,
@Nonnull final Iterable<T> values)
throws IOException {
Objects.requireNonNull(values);
try (final InputStream stream =
serializeAsStream(contentType, serializeOnlyChangedValues, values)) {
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,5 +1,7 @@
package com.microsoft.kiota.serialization;

import com.microsoft.kiota.store.BackingStoreSerializationWriterProxyFactory;

import jakarta.annotation.Nonnull;

import java.util.HashMap;
Expand Down Expand Up @@ -36,20 +38,92 @@ public SerializationWriterFactoryRegistry() {
if (contentType.isEmpty()) {
throw new NullPointerException("contentType cannot be empty");
}
final String vendorSpecificContentType = contentType.split(";")[0];
final ContentTypeWrapper contentTypeWrapper = new ContentTypeWrapper(contentType);
Ndiritu marked this conversation as resolved.
Show resolved Hide resolved
final SerializationWriterFactory serializationWriterFactory =
getSerializationWriterFactory(contentTypeWrapper);
return serializationWriterFactory.getSerializationWriter(
contentTypeWrapper.cleanedContentType);
}

/**
* Get a Serialization Writer with backing store configured with serializeOnlyChangedValues
* @param contentType
* @param serializeOnlyChangedValues control backing store functionality
* @return the serialization writer
*/
@Nonnull public SerializationWriter getSerializationWriter(
@Nonnull final String contentType, final boolean serializeOnlyChangedValues) {
if (!serializeOnlyChangedValues) {
final ContentTypeWrapper contentTypeWrapper = new ContentTypeWrapper(contentType);
final SerializationWriterFactory factory =
getSerializationWriterFactory(contentTypeWrapper);
if (factory instanceof BackingStoreSerializationWriterProxyFactory) {
return ((BackingStoreSerializationWriterProxyFactory) factory)
.getSerializationWriter(
contentTypeWrapper.cleanedContentType, serializeOnlyChangedValues);
}
}
return getSerializationWriter(contentType);
}

/**
* Gets a SerializationWriterFactory that is mapped to a cleaned content type string
* @param contentTypeWrapper wrapper object carrying initial content type and result of parsing it
* @return the serialization writer factory
* @throws RuntimeException when no mapped factory is found
*/
@Nonnull private SerializationWriterFactory getSerializationWriterFactory(
@Nonnull final ContentTypeWrapper contentTypeWrapper) {
final String vendorSpecificContentType =
getVendorSpecificContentType(contentTypeWrapper.contentType);
if (contentTypeAssociatedFactories.containsKey(vendorSpecificContentType)) {
return contentTypeAssociatedFactories
.get(vendorSpecificContentType)
.getSerializationWriter(vendorSpecificContentType);
contentTypeWrapper.cleanedContentType = vendorSpecificContentType;
return contentTypeAssociatedFactories.get(contentTypeWrapper.cleanedContentType);
}
final String cleanedContentType =
contentTypeVendorCleanupPattern.matcher(vendorSpecificContentType).replaceAll("");
getCleanedVendorSpecificContentType(vendorSpecificContentType);
if (contentTypeAssociatedFactories.containsKey(cleanedContentType)) {
return contentTypeAssociatedFactories
.get(cleanedContentType)
.getSerializationWriter(cleanedContentType);
contentTypeWrapper.cleanedContentType = cleanedContentType;
return contentTypeAssociatedFactories.get(contentTypeWrapper.cleanedContentType);
}
throw new RuntimeException(
"Content type " + contentType + " does not have a factory to be serialized");
"Content type "
+ contentTypeWrapper.contentType
+ " does not have a factory to be serialized");
}

/**
* 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("");
}

/**
* Wrapper class to carry the cleaned version of content-type after parsing in multiple stages
*/
private static final class ContentTypeWrapper {
String contentType;
String cleanedContentType;

ContentTypeWrapper(@Nonnull final String contentType) {
this.contentType = contentType;
this.cleanedContentType = "";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public abstract class SerializationWriterProxyFactory implements SerializationWr
return _concrete.getValidContentType();
}

private final SerializationWriterFactory _concrete;
protected final SerializationWriterFactory _concrete;
Ndiritu marked this conversation as resolved.
Show resolved Hide resolved
private final Consumer<Parsable> _onBefore;
private final Consumer<Parsable> _onAfter;
private final BiConsumer<Parsable, SerializationWriter> _onStart;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.microsoft.kiota.store;

import com.microsoft.kiota.serialization.SerializationWriter;
import com.microsoft.kiota.serialization.SerializationWriterFactory;
import com.microsoft.kiota.serialization.SerializationWriterProxyFactory;

Expand Down Expand Up @@ -48,4 +49,21 @@ public BackingStoreSerializationWriterProxyFactory(
}
});
}

/**
* Returns a SerializationWriter that overrides the default serialization of only changed values if serializeOnlyChangedValues="true"
* Gets the previously proxied serialization writer without any backing store configuration to prevent overwriting the registry affecting
* future serialization requests
*
* @param contentType HTTP content type header value
* @param serializeOnlyChangedValues alter backing store default behavior
* @return the SerializationWriter
*/
@Nonnull public SerializationWriter getSerializationWriter(
@Nonnull final String contentType, final boolean serializeOnlyChangedValues) {
if (!serializeOnlyChangedValues) {
return _concrete.getSerializationWriter(contentType);
}
return getSerializationWriter(contentType);
}
}
Loading
Loading