diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 420409ac3..23229986f 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -20,6 +20,11 @@ updates:
schedule:
interval: daily
open-pull-requests-limit: 10
+- package-ecosystem: gradle
+ directory: "/components/serialization/multipart"
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 10
- package-ecosystem: gradle
directory: "/components/authentication/azure"
schedule:
@@ -50,6 +55,11 @@ updates:
schedule:
interval: daily
open-pull-requests-limit: 10
+- package-ecosystem: gradle
+ directory: "/components/serialization/multipart/android"
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 10
- package-ecosystem: gradle
directory: "/components/authentication/azure/android"
schedule:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index acdc91366..4a444d6c6 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -46,6 +46,7 @@ jobs:
Copy-Item secring.gpg components/serialization/form/ -Verbose
Copy-Item secring.gpg components/serialization/text/ -Verbose
Copy-Item secring.gpg components/serialization/json/ -Verbose
+ Copy-Item secring.gpg components/serialization/multipart/ -Verbose
Copy-Item secring.gpg components/http/okHttp/ -Verbose
shell: pwsh
working-directory: ./
@@ -90,6 +91,7 @@ jobs:
Copy-Item secring.gpg components/serialization/form/ -Verbose
Copy-Item secring.gpg components/serialization/text/ -Verbose
Copy-Item secring.gpg components/serialization/json/ -Verbose
+ Copy-Item secring.gpg components/serialization/multipart/ -Verbose
Copy-Item secring.gpg components/http/okHttp/ -Verbose
shell: pwsh
working-directory: ./
@@ -108,6 +110,9 @@ jobs:
- name: Publish Release serialization text
run: ./gradlew --no-daemon :components:serialization:text:$PUBLISH_TASK -PmavenCentralSnapshotArtifactSuffix=""
working-directory: ./
+ - name: Publish Release serialization multipart
+ run: ./gradlew --no-daemon :components:serialization:multipart:$PUBLISH_TASK -PmavenCentralSnapshotArtifactSuffix=""
+ working-directory: ./
- name: Publish Release authentication azure
run: ./gradlew --no-daemon :components:authentication:azure:$PUBLISH_TASK -PmavenCentralSnapshotArtifactSuffix=""
working-directory: ./
diff --git a/CHANGELOG.md b/CHANGELOG.md
index be1672993..d2071ecd3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+## [0.5.0] - 2023-07-26
+
+### Added
+
+- Added support for multipart form data request bodies.
+
## [0.4.7] - 2023-07-21
### Added
diff --git a/README.md b/README.md
index 2215ce983..1cc77ead1 100644
--- a/README.md
+++ b/README.md
@@ -4,12 +4,13 @@
The Kiota Java Libraries for Java are:
- - [abstractions] Defining the basic constructs Kiota projects need once an SDK has been generated from an OpenAPI definition
- - [authentication/azure] Implementing Azure authentication mechanisms
- - [http/okHttp] Implementing a default OkHttp client
- - [serialization/form] Implementing default serialization for forms
- - [serialization/json] Implementing default serialization for json
- - [serialization/text] Implementing default serialization for text
+- [abstractions] Defining the basic constructs Kiota projects need once an SDK has been generated from an OpenAPI definition
+- [authentication/azure] Implementing Azure authentication mechanisms
+- [http/okHttp] Implementing a default OkHttp client
+- [serialization/form] Implementing default serialization for forms
+- [serialization/json] Implementing default serialization for json
+- [serialization/text] Implementing default serialization for text
+- [serialization/multipart] Implementing default serialization for multipart
Read more about Kiota [here](https://github.com/microsoft/kiota/blob/main/README.md).
@@ -20,12 +21,13 @@ Read more about Kiota [here](https://github.com/microsoft/kiota/blob/main/README
In `build.gradle` in the `dependencies` section:
```Groovy
-implementation 'com.microsoft.kiota:microsoft-kiota-abstractions:0.4.0'
-implementation 'com.microsoft.kiota:microsoft-kiota-authentication-azure:0.4.0'
-implementation 'com.microsoft.kiota:microsoft-kiota-http-okHttp:0.4.0'
-implementation 'com.microsoft.kiota:microsoft-kiota-serialization-json:0.4.0'
-implementation 'com.microsoft.kiota:microsoft-kiota-serialization-text:0.4.0'
-implementation 'com.microsoft.kiota:microsoft-kiota-serialization-form:0.4.0'
+implementation 'com.microsoft.kiota:microsoft-kiota-abstractions:0.5.0'
+implementation 'com.microsoft.kiota:microsoft-kiota-authentication-azure:0.5.0'
+implementation 'com.microsoft.kiota:microsoft-kiota-http-okHttp:0.5.0'
+implementation 'com.microsoft.kiota:microsoft-kiota-serialization-json:0.5.0'
+implementation 'com.microsoft.kiota:microsoft-kiota-serialization-text:0.5.0'
+implementation 'com.microsoft.kiota:microsoft-kiota-serialization-form:0.5.0'
+implementation 'com.microsoft.kiota:microsoft-kiota-serialization-multipart:0.5.0'
```
### With Maven:
@@ -36,32 +38,37 @@ In `pom.xml` in the `dependencies` section:
com.microsoft.kiota
microsoft-kiota-abstractions
- 0.4.0
+ 0.5.0
com.microsoft.kiota
microsoft-kiota-authentication-azure
- 0.4.0
+ 0.5.0
com.microsoft.kiota
microsoft-kiota-http-okHttp
- 0.4.0
+ 0.5.0
com.microsoft.kiota
microsoft-kiota-serialization-json
- 0.4.0
+ 0.5.0
com.microsoft.kiota
microsoft-kiota-serialization-text
- 0.4.0
+ 0.5.0
com.microsoft.kiota
microsoft-kiota-serialization-form
- 0.4.0
+ 0.5.0
+
+
+ com.microsoft.kiota
+ microsoft-kiota-serialization-multipart
+ 0.5.0
```
diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/MultipartBody.java b/components/abstractions/src/main/java/com/microsoft/kiota/MultipartBody.java
new file mode 100644
index 000000000..984c8eaae
--- /dev/null
+++ b/components/abstractions/src/main/java/com/microsoft/kiota/MultipartBody.java
@@ -0,0 +1,168 @@
+package com.microsoft.kiota;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.function.Consumer;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.google.common.base.Strings;
+import com.microsoft.kiota.serialization.Parsable;
+import com.microsoft.kiota.serialization.ParseNode;
+import com.microsoft.kiota.serialization.SerializationWriter;
+import com.microsoft.kiota.serialization.SerializationWriterFactory;
+
+/**
+ * Represents a multipart body for a request or a response.
+ */
+public class MultipartBody implements Parsable {
+ @Nonnull
+ private final String boundary = UUID.randomUUID().toString().replace("-", "");
+ /** @return the boundary string for the multipart body. */
+ @Nonnull
+ public String getBoundary() {
+ return boundary;
+ }
+ /**
+ * The request adapter to use for the multipart body serialization.
+ */
+ @Nullable
+ public RequestAdapter requestAdapter;
+ /**
+ * Adds or replaces a part in the multipart body.
+ * @param the type of the part to add or replace.
+ * @param name the name of the part to add or replace.
+ * @param contentType the content type of the part to add or replace.
+ * @param value the value of the part to add or replace.
+ */
+ public void addOrReplacePart(@Nonnull final String name, @Nonnull final String contentType, @Nonnull final T value) {
+ Objects.requireNonNull(value);
+ if (Strings.isNullOrEmpty(contentType) || contentType.isBlank())
+ throw new IllegalArgumentException("contentType cannot be blank or empty");
+ if (Strings.isNullOrEmpty(name) || name.isBlank())
+ throw new IllegalArgumentException("name cannot be blank or empty");
+
+ final String normalizedName = normalizePartName(name);
+ originalNames.put(normalizedName, name);
+ parts.put(normalizedName, Map.entry(contentType, value));
+ }
+ private final Map> parts = new HashMap<>();
+ private final Map originalNames = new HashMap<>();
+ private String normalizePartName(@Nonnull final String original)
+ {
+ return original.toLowerCase(Locale.ROOT);
+ }
+ /**
+ * Gets the content type of the part with the specified name.
+ * @param partName the name of the part to get.
+ * @return the content type of the part with the specified name.
+ */
+ @Nullable
+ public Object getPartValue(@Nonnull final String partName)
+ {
+ if (Strings.isNullOrEmpty(partName) || partName.isBlank())
+ throw new IllegalArgumentException("partName cannot be blank or empty");
+ final String normalizedName = normalizePartName(partName);
+ final Map.Entry candidate = parts.get(normalizedName);
+ if(candidate == null)
+ return null;
+ return candidate.getValue();
+ }
+ /**
+ * Gets the content type of the part with the specified name.
+ * @param partName the name of the part to get.
+ * @return the content type of the part with the specified name.
+ */
+ public boolean removePart(@Nonnull final String partName)
+ {
+ if (Strings.isNullOrEmpty(partName) || partName.isBlank())
+ throw new IllegalArgumentException("partName cannot be blank or empty");
+ final String normalizedName = normalizePartName(partName);
+ final Object candidate = parts.remove(normalizedName);
+ if(candidate == null)
+ return false;
+
+ originalNames.remove(normalizedName);
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull
+ public Map> getFieldDeserializers() {
+ throw new UnsupportedOperationException("Unimplemented method 'getFieldDeserializers'");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void serialize(@Nonnull final SerializationWriter writer) {
+ Objects.requireNonNull(writer);
+ final RequestAdapter ra = requestAdapter;
+ if (ra == null)
+ throw new IllegalStateException("requestAdapter cannot be null");
+ if (parts.isEmpty())
+ throw new IllegalStateException("multipart body cannot be empty");
+ final SerializationWriterFactory serializationFactory = ra.getSerializationWriterFactory();
+ boolean isFirst = true;
+ for (final Map.Entry> partEntry : parts.entrySet()) {
+ try {
+ if (isFirst)
+ isFirst = false;
+ else
+ writer.writeStringValue("", "");
+ writer.writeStringValue("", "--" + getBoundary());
+ final String partContentType = partEntry.getValue().getKey();
+ writer.writeStringValue("Content-Type", partContentType);
+ writer.writeStringValue("Content-Disposition", "form-data; name=\"" + originalNames.get(partEntry.getKey()) + "\"");
+ writer.writeStringValue("", "");
+ final Object objectValue = partEntry.getValue().getValue();
+ if (objectValue instanceof Parsable) {
+ try (final SerializationWriter partWriter = serializationFactory.getSerializationWriter(partContentType)) {
+ partWriter.writeObjectValue("", ((Parsable)objectValue));
+ try (final InputStream partContent = partWriter.getSerializedContent()) {
+ if (partContent.markSupported())
+ partContent.reset();
+ writer.writeByteArrayValue("", readAllBytes(partContent));
+ }
+ }
+ } else if (objectValue instanceof String) {
+ writer.writeStringValue("", (String)objectValue);
+ } else if (objectValue instanceof InputStream) {
+ final InputStream inputStream = (InputStream)objectValue;
+ if (inputStream.markSupported())
+ inputStream.reset();
+ writer.writeByteArrayValue("", readAllBytes(inputStream));
+ } else if (objectValue instanceof byte[]) {
+ writer.writeByteArrayValue("", (byte[])objectValue);
+ } else {
+ throw new IllegalStateException("Unsupported part type" + objectValue.getClass().getName());
+ }
+ } catch (final IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+ writer.writeStringValue("", "");
+ writer.writeStringValue("", "--" + boundary + "--");
+ }
+ @Nonnull
+ private byte[] readAllBytes(@Nonnull final InputStream inputStream) throws IOException {
+ // InputStream.readAllBytes() is only available to Android API level 33+
+ final int bufLen = 1024;
+ byte[] buf = new byte[bufLen];
+ int readLen;
+ try(final ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+ while ((readLen = inputStream.read(buf, 0, bufLen)) != -1)
+ outputStream.write(buf, 0, readLen);
+ return outputStream.toByteArray();
+ }
+ }
+}
diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java b/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java
index da31f1d53..730af0c85 100644
--- a/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java
+++ b/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java
@@ -240,7 +240,13 @@ public void setContentFromParsable(@Nonnull final RequestAd
final Span span = GlobalOpenTelemetry.getTracer(OBSERVABILITY_TRACER_NAME).spanBuilder(SPAN_NAME).startSpan();
try (final Scope scope = span.makeCurrent()) {
try(final SerializationWriter writer = getSerializationWriter(requestAdapter, contentType, value)) {
- headers.add(CONTENT_TYPE_HEADER, contentType);
+ String effectiveContentType = contentType;
+ if (value instanceof MultipartBody) {
+ final MultipartBody multipartBody = (MultipartBody)value;
+ effectiveContentType += "; boundary=" + multipartBody.getBoundary();
+ multipartBody.requestAdapter = requestAdapter;
+ }
+ headers.add(CONTENT_TYPE_HEADER, effectiveContentType);
setRequestType(value, span);
writer.writeObjectValue(null, value);
this.content = writer.getSerializedContent();
diff --git a/components/abstractions/src/test/java/com/microsoft/kiota/MultiPartBodyTest.java b/components/abstractions/src/test/java/com/microsoft/kiota/MultiPartBodyTest.java
new file mode 100644
index 000000000..5679cb2b9
--- /dev/null
+++ b/components/abstractions/src/test/java/com/microsoft/kiota/MultiPartBodyTest.java
@@ -0,0 +1,60 @@
+package com.microsoft.kiota;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import org.junit.jupiter.api.Test;
+
+import com.microsoft.kiota.serialization.SerializationWriter;
+
+class MultiPartBodyTest {
+ @Test
+ void defensive() {
+ final MultipartBody multipartBody = new MultipartBody();
+ assertThrows(IllegalArgumentException.class, () -> multipartBody.addOrReplacePart(null, "foo", "bar"));
+ assertThrows(IllegalArgumentException.class, () -> multipartBody.addOrReplacePart("foo", null, "bar"));
+ assertThrows(NullPointerException.class, () -> multipartBody.addOrReplacePart("foo", "bar", null));
+ assertThrows(IllegalArgumentException.class, () -> multipartBody.getPartValue(null));
+ assertThrows(IllegalArgumentException.class, () -> multipartBody.removePart(null));
+ assertThrows(NullPointerException.class, () -> multipartBody.serialize(null));
+ assertThrows(UnsupportedOperationException.class, () -> multipartBody.getFieldDeserializers());
+ }
+ @Test
+ void requiresRequestAdapter(){
+ final MultipartBody multipartBody = new MultipartBody();
+ final SerializationWriter writer = mock(SerializationWriter.class);
+ assertThrows(IllegalStateException.class, () -> multipartBody.serialize(writer));
+ }
+ @Test
+ void requiresPartsForSerialization() {
+ final MultipartBody multipartBody = new MultipartBody();
+ final SerializationWriter writer = mock(SerializationWriter.class);
+ final RequestAdapter requestAdapter = mock(RequestAdapter.class);
+ multipartBody.requestAdapter = requestAdapter;
+ assertThrows(IllegalStateException.class, () -> multipartBody.serialize(writer));
+ }
+ @Test
+ void addsPart() {
+ final MultipartBody multipartBody = new MultipartBody();
+ final RequestAdapter requestAdapter = mock(RequestAdapter.class);
+ multipartBody.requestAdapter = requestAdapter;
+ multipartBody.addOrReplacePart("foo", "bar", "baz");
+ final Object result = multipartBody.getPartValue("foo");
+ assertNotNull(result);
+ assertTrue(result instanceof String);
+ }
+ @Test
+ void removesPart() {
+ final MultipartBody multipartBody = new MultipartBody();
+ final RequestAdapter requestAdapter = mock(RequestAdapter.class);
+ multipartBody.requestAdapter = requestAdapter;
+ multipartBody.addOrReplacePart("foo", "bar", "baz");
+ multipartBody.removePart("FOO");
+ final Object result = multipartBody.getPartValue("foo");
+ assertNull(result);
+ }
+ // serialize method is being tested in the serialization library
+}
diff --git a/components/abstractions/src/test/java/com/microsoft/kiota/RequestInformationTest.java b/components/abstractions/src/test/java/com/microsoft/kiota/RequestInformationTest.java
index de963c85f..1112e5c6f 100644
--- a/components/abstractions/src/test/java/com/microsoft/kiota/RequestInformationTest.java
+++ b/components/abstractions/src/test/java/com/microsoft/kiota/RequestInformationTest.java
@@ -18,9 +18,9 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-public class RequestInformationTest {
+class RequestInformationTest {
@Test
- public void ThrowsInvalidOperationExceptionWhenBaseUrlNotSet()
+ void ThrowsInvalidOperationExceptionWhenBaseUrlNotSet()
{
// Arrange as the request builders would
final RequestInformation requestInfo = new RequestInformation();
@@ -33,7 +33,7 @@ public void ThrowsInvalidOperationExceptionWhenBaseUrlNotSet()
}
@Test
- public void BuildsUrlOnProvidedBaseUrl()
+ void BuildsUrlOnProvidedBaseUrl()
{
// Arrange as the request builders would
final RequestInformation requestInfo = new RequestInformation();
@@ -49,7 +49,7 @@ public void BuildsUrlOnProvidedBaseUrl()
}
@Test
- public void SetsPathParametersOfDateTimeOffsetType()
+ void SetsPathParametersOfDateTimeOffsetType()
{
// Arrange as the request builders would
final RequestInformation requestInfo = new RequestInformation();
@@ -69,7 +69,7 @@ public void SetsPathParametersOfDateTimeOffsetType()
}
@Test
- public void ExpandQueryParametersAfterPathParams()
+ void ExpandQueryParametersAfterPathParams()
{
// Arrange as the request builders would
final RequestInformation requestInfo = new RequestInformation();
@@ -89,7 +89,7 @@ public void ExpandQueryParametersAfterPathParams()
}
@Test
- public void DoesNotSetQueryParametersParametersIfEmptyString()
+ void DoesNotSetQueryParametersParametersIfEmptyString()
{
// Arrange as the request builders would
@@ -111,7 +111,7 @@ public void DoesNotSetQueryParametersParametersIfEmptyString()
}
@Test
- public void DoesNotSetQueryParametersParametersIfEmptyCollection()
+ void DoesNotSetQueryParametersParametersIfEmptyCollection()
{
// Arrange as the request builders would
@@ -131,7 +131,7 @@ public void DoesNotSetQueryParametersParametersIfEmptyCollection()
}
@Test
- public void SetsPathParametersOfBooleanType()
+ void SetsPathParametersOfBooleanType()
{
// Arrange as the request builders would
@@ -148,7 +148,7 @@ public void SetsPathParametersOfBooleanType()
assertTrue(uriResult.toString().contains("count=true"));
}
@Test
- public void SetsParsableContent() {
+ void SetsParsableContent() {
// Arrange as the request builders would
final RequestInformation requestInfo = new RequestInformation();
requestInfo.httpMethod= HttpMethod.POST;
@@ -164,7 +164,7 @@ public void SetsParsableContent() {
verify(writerMock, never()).writeCollectionOfObjectValues(anyString(), any(ArrayList.class));
}
@Test
- public void SetsParsableContentCollection() {
+ void SetsParsableContentCollection() {
// Arrange as the request builders would
final RequestInformation requestInfo = new RequestInformation();
requestInfo.httpMethod= HttpMethod.POST;
@@ -180,7 +180,7 @@ public void SetsParsableContentCollection() {
verify(writerMock, times(1)).writeCollectionOfObjectValues(any(), any(Iterable.class));
}
@Test
- public void SetsScalarContentCollection() {
+ void SetsScalarContentCollection() {
// Arrange as the request builders would
final RequestInformation requestInfo = new RequestInformation();
requestInfo.httpMethod= HttpMethod.POST;
@@ -196,7 +196,7 @@ public void SetsScalarContentCollection() {
verify(writerMock, times(1)).writeCollectionOfPrimitiveValues(any(), any(Iterable.class));
}
@Test
- public void SetsScalarContent() {
+ void SetsScalarContent() {
// Arrange as the request builders would
final RequestInformation requestInfo = new RequestInformation();
requestInfo.httpMethod= HttpMethod.POST;
@@ -211,6 +211,25 @@ public void SetsScalarContent() {
verify(writerMock, times(1)).writeStringValue(any(), anyString());
verify(writerMock, never()).writeCollectionOfPrimitiveValues(any(), any(Iterable.class));
}
+ @Test
+ void SetsBoundaryOnMultipartBody() {
+ final RequestInformation requestInfo = new RequestInformation();
+ requestInfo.httpMethod= HttpMethod.POST;
+ requestInfo.urlTemplate = "http://localhost/{URITemplate}/ParameterMapping?IsCaseSensitive={IsCaseSensitive}";
+ final SerializationWriter writerMock = mock(SerializationWriter.class);
+ final SerializationWriterFactory factoryMock = mock(SerializationWriterFactory.class);
+ when(factoryMock.getSerializationWriter(anyString())).thenReturn(writerMock);
+ final RequestAdapter requestAdapterMock = mock(RequestAdapter.class);
+ when(requestAdapterMock.getSerializationWriterFactory()).thenReturn(factoryMock);
+
+ final MultipartBody multipartBody = new MultipartBody();
+ multipartBody.requestAdapter = requestAdapterMock;
+
+ requestInfo.setContentFromParsable(requestAdapterMock, "multipart/form-data", multipartBody);
+ assertNotNull(multipartBody.getBoundary());
+ assertFalse(multipartBody.getBoundary().isEmpty());
+ assertEquals("multipart/form-data; boundary=" + multipartBody.getBoundary(), requestInfo.headers.get("Content-Type").toArray()[0]);
+ }
}
diff --git a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/UserAgentHandlerOption.java b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/UserAgentHandlerOption.java
index 3c85b0924..9f0163ea9 100644
--- a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/UserAgentHandlerOption.java
+++ b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/UserAgentHandlerOption.java
@@ -14,7 +14,7 @@ public UserAgentHandlerOption() { }
@Nonnull
private String productName = "kiota-java";
@Nonnull
- private String productVersion = "0.2.0";
+ private String productVersion = "0.5.0";
/**
* Gets the product name to be used in the user agent header
* @return the product name
diff --git a/components/serialization/multipart/.gitignore b/components/serialization/multipart/.gitignore
new file mode 100644
index 000000000..20a548b24
--- /dev/null
+++ b/components/serialization/multipart/.gitignore
@@ -0,0 +1,10 @@
+# Ignore Gradle project-specific cache directory
+.gradle
+
+# Ignore Gradle build output directory
+build
+
+
+.settings
+.project
+.classpath
\ No newline at end of file
diff --git a/components/serialization/multipart/android/.gitignore b/components/serialization/multipart/android/.gitignore
new file mode 100644
index 000000000..f06dfad69
--- /dev/null
+++ b/components/serialization/multipart/android/.gitignore
@@ -0,0 +1,2 @@
+.gradle
+build
\ No newline at end of file
diff --git a/components/serialization/multipart/android/AndroidManifest.xml b/components/serialization/multipart/android/AndroidManifest.xml
new file mode 100644
index 000000000..2932f8879
--- /dev/null
+++ b/components/serialization/multipart/android/AndroidManifest.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/components/serialization/multipart/android/build.gradle b/components/serialization/multipart/android/build.gradle
new file mode 100644
index 000000000..29f8f6f8c
--- /dev/null
+++ b/components/serialization/multipart/android/build.gradle
@@ -0,0 +1,73 @@
+buildscript {
+ repositories {
+ google()
+ gradlePluginPortal()
+ maven {
+ url "https://plugins.gradle.org/m2/"
+ }
+ maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
+ }
+
+ dependencies {
+ classpath "com.gradle:gradle-enterprise-gradle-plugin:3.14"
+ classpath "com.android.tools.build:gradle:8.1.0"
+ classpath "com.github.ben-manes:gradle-versions-plugin:0.47.0"
+ }
+}
+
+repositories {
+ google()
+ gradlePluginPortal()
+}
+
+apply plugin: "com.android.library"
+apply plugin: "com.github.ben-manes.versions"
+
+android {
+ namespace "com.microsoft.kiota.serialization"
+ compileSdkVersion 34
+
+ defaultConfig {
+ versionCode 1
+ versionName "1.0"
+ minSdkVersion 26
+ targetSdkVersion 34
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ lintOptions {
+ textOutput "stdout"
+ checkAllWarnings true
+ warningsAsErrors true
+ disable "UnusedResources" // Unused will be removed on release
+ disable "IconExpectedSize" // Using the material icons provided from Google
+ disable "GoogleAppIndexingApiWarning" // We might want to index our app later
+ disable "InvalidPackage" // Butterknife, Okio and Realm
+ disable "ResourceType" // Annotation binding
+ disable "GradleDependency"
+ disable "NewerVersionAvailable"
+ disable "DuplicatePlatformClasses" // xpp3 added by azure-identity
+ }
+ sourceSets {
+ main {
+ java.srcDirs = ['../src/main/java']
+ res.srcDirs = ['../src/main/java']
+ manifest.srcFile 'AndroidManifest.xml'
+ }
+ androidTest {
+ setRoot '../src/test'
+ }
+ }
+}
+
+apply from: "../gradle/dependencies.gradle"
diff --git a/components/serialization/multipart/android/gradle.properties b/components/serialization/multipart/android/gradle.properties
new file mode 100644
index 000000000..c4b9317c4
--- /dev/null
+++ b/components/serialization/multipart/android/gradle.properties
@@ -0,0 +1 @@
+mavenArtifactId = microsoft-kiota-serialization-multipart
diff --git a/components/serialization/multipart/build.gradle b/components/serialization/multipart/build.gradle
new file mode 100644
index 000000000..7eeb6a51a
--- /dev/null
+++ b/components/serialization/multipart/build.gradle
@@ -0,0 +1,239 @@
+plugins {
+ // Apply the java-library plugin to add support for Java Library
+ id 'java-library'
+ id 'java'
+ id 'eclipse'
+ id 'maven-publish'
+ id 'signing'
+ id 'jacoco'
+ id 'com.github.spotbugs' version '5.0.14'
+ id "org.sonarqube" version "4.3.0.3225"
+}
+
+java {
+ modularity.inferModulePath = true
+ withSourcesJar()
+ withJavadocJar()
+}
+
+test {
+ useJUnitPlatform()
+ finalizedBy jacocoTestReport // report is always generated after tests run
+}
+
+jacocoTestReport {
+ dependsOn test // tests are required to run before generating the report
+}
+
+jacoco {
+ toolVersion = "0.8.10"
+}
+
+spotbugsMain {
+ excludeFilter = file("spotBugsExcludeFilter.xml")
+ reports {
+ html {
+ required
+ outputLocation = file("$buildDir/reports/spotbugs/main/spotbugs.html")
+ stylesheet = 'fancy-hist.xsl'
+ }
+ }
+}
+
+spotbugsTest {
+ excludeFilter = file("spotBugsExcludeFilter.xml")
+ reports {
+ html {
+ required
+ outputLocation = file("$buildDir/reports/spotbugs/test/spotbugs.html")
+ stylesheet = 'fancy-hist.xsl'
+ }
+ }
+}
+
+jacocoTestReport {
+ reports {
+ xml.required
+ }
+}
+
+sourceSets {
+ main {
+ java {
+ exclude 'pom.xml'
+ }
+ }
+}
+
+// In this section you declare where to find the dependencies of your project
+repositories {
+ // You can declare any Maven/Ivy/file repository here.
+ mavenCentral()
+}
+
+apply from: "gradle/dependencies.gradle"
+
+def pomConfig = {
+ licenses {
+ license([:]) {
+ name "MIT License"
+ url "http://opensource.org/licenses/MIT"
+ distribution "repo"
+ }
+ }
+}
+
+sonarqube {
+ properties {
+ property "sonar.projectKey", "microsoft_kiota-java"
+ property "sonar.organization", "microsoft"
+ property "sonar.host.url", "https://sonarcloud.io"
+ }
+}
+
+//Publishing tasks-
+//Maven Central Snapshot: publishMavenPublicationToMavenRepository
+//Maven Central Release: publishmavenPublicationToMaven2Repository
+
+tasks.jar {
+ manifest {
+ attributes('Automatic-Module-Name': project.property('mavenGroupId'))
+ }
+}
+
+publishing {
+
+ publications {
+ maven(MavenPublication) {
+ customizePom(pom)
+ groupId project.property('mavenGroupId')
+ artifactId project.property('mavenArtifactId')
+ version "${mavenMajorVersion}.${mavenMinorVersion}.${mavenPatchVersion}${mavenCentralSnapshotArtifactSuffix}"
+ from components.java
+ pom.withXml {
+ def pomFile = file("${project.buildDir}/generated-pom.xml")
+ writeTo(pomFile)
+ }
+ }
+ }
+ repositories {
+ maven {
+ url = 'https://oss.sonatype.org/content/repositories/snapshots'
+ name = 'sonatypeSnapshot'
+
+ credentials {
+ if (project.rootProject.file('local.properties').exists()) {
+ Properties properties = new Properties()
+ properties.load(project.rootProject.file('local.properties').newDataInputStream())
+ username = properties.getProperty('sonatypeUsername')
+ password = properties.getProperty('sonatypePassword')
+ }
+ }
+ }
+
+ maven {
+ url = 'https://oss.sonatype.org/service/local/staging/deploy/maven2'
+ name = 'sonatype'
+
+ credentials {
+ if (project.rootProject.file('local.properties').exists()) {
+ Properties properties = new Properties()
+ properties.load(project.rootProject.file('local.properties').newDataInputStream())
+ username = properties.getProperty('sonatypeUsername')
+ password = properties.getProperty('sonatypePassword')
+ }
+ }
+ }
+ }
+}
+
+signing {
+ sign publishing.publications.maven
+}
+tasks.withType(Sign)*.enabled = mavenCentralPublishingEnabled.toBoolean()
+
+def fixAscNames = { name ->
+ if(name.contains('pom')) {
+ "${project.property('mavenArtifactId')}-${mavenMajorVersion}.${mavenMinorVersion}.${mavenPatchVersion}.pom.asc"
+ } else {
+ name.replace('microsoft-kiota-java-serialization-multipart', "${project.property('mavenArtifactId')}-${mavenMajorVersion}.${mavenMinorVersion}.${mavenPatchVersion}")
+ }
+}
+
+compileJava {
+ options.compilerArgs << "-parameters"
+ sourceCompatibility = '1.8'
+ targetCompatibility = '1.8'
+}
+
+def getVersionCode() {
+ return mavenMajorVersion.toInteger() * 10000 + mavenMinorVersion.toInteger() * 100 + mavenPatchVersion.toInteger()
+}
+
+def getVersionName() {
+ return "${mavenMajorVersion}.${mavenMinorVersion}.${mavenPatchVersion}${mavenArtifactSuffix}"
+}
+
+artifacts {
+ archives jar
+}
+
+def customizePom(pom) {
+ pom.withXml {
+ def root = asNode()
+
+ root.dependencies.removeAll { dep ->
+ dep.scope == "test"
+ }
+
+ root.children().last() + {
+ resolveStrategy = Closure.DELEGATE_FIRST
+
+ description 'Microsoft Kiota-Serialization for Multipart'
+ name 'Microsoft Kiota-Java Serialization-Multipart'
+ url 'https://github.com/microsoft/kiota-java'
+ organization {
+ name 'Microsoft'
+ url 'https://github.com/microsoft/kiota-java'
+ }
+ issueManagement {
+ system 'GitHub'
+ url 'https://github.com/microsoft/kiota-java/issues'
+ }
+ licenses {
+ license {
+ name "MIT License"
+ url "http://opensource.org/licenses/MIT"
+ distribution "repo"
+ }
+ }
+ scm {
+ url 'https://github.com/microsoft/kiota-java'
+ connection 'scm:git:git://github.com/microsoft/kiota-java.git'
+ developerConnection 'scm:git:ssh://git@github.com:microsoft/kiota-java.git'
+ }
+ developers {
+ developer {
+ name 'Microsoft'
+ }
+ }
+ }
+ }
+}
+
+gradle.taskGraph.whenReady { taskGraph ->
+ if (project.rootProject.file('local.properties').exists()) {
+ Properties properties = new Properties()
+ properties.load(project.rootProject.file('local.properties').newDataInputStream())
+ tasks.withType(Sign)*.enabled = (properties.containsKey('enableSigning')) ? properties.getProperty('enableSigning').toBoolean() : false
+ allprojects { ext."signing.keyId" = properties.getProperty('signing.keyId') }
+ allprojects { ext."signing.secretKeyRingFile" = properties.getProperty('signing.secretKeyRingFile') }
+ allprojects { ext."signing.password" = properties.getProperty('signing.password') }
+ }
+}
+
+model {
+ tasks.generatePomFileForMavenPublication {
+ destination = file("${project.buildDir}/generated-pom.xml")
+ }
+}
\ No newline at end of file
diff --git a/components/serialization/multipart/gradle.properties b/components/serialization/multipart/gradle.properties
new file mode 100644
index 000000000..c4b9317c4
--- /dev/null
+++ b/components/serialization/multipart/gradle.properties
@@ -0,0 +1 @@
+mavenArtifactId = microsoft-kiota-serialization-multipart
diff --git a/components/serialization/multipart/gradle/dependencies.gradle b/components/serialization/multipart/gradle/dependencies.gradle
new file mode 100644
index 000000000..6ef8cf374
--- /dev/null
+++ b/components/serialization/multipart/gradle/dependencies.gradle
@@ -0,0 +1,14 @@
+dependencies {
+ // Use JUnit Jupiter API for testing.
+ testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0'
+ testImplementation 'org.mockito:mockito-inline:5.2.0'
+
+ // 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 'com.google.guava:guava:32.1.2-jre'
+
+ api project(':components:abstractions')
+ testImplementation project(':components:serialization:json')
+}
diff --git a/components/serialization/multipart/spotBugsExcludeFilter.xml b/components/serialization/multipart/spotBugsExcludeFilter.xml
new file mode 100644
index 000000000..d9952e56b
--- /dev/null
+++ b/components/serialization/multipart/spotBugsExcludeFilter.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java b/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java
new file mode 100644
index 000000000..29e40e96f
--- /dev/null
+++ b/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java
@@ -0,0 +1,174 @@
+package com.microsoft.kiota.serialization;
+
+import com.microsoft.kiota.PeriodAndDuration;
+import com.microsoft.kiota.MultipartBody;
+
+import java.math.BigDecimal;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.OffsetDateTime;
+import java.util.Base64;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+import java.util.function.BiConsumer;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/** Serialization writer implementation for Multipart encoded payloads */
+public class MultipartSerializationWriter implements SerializationWriter {
+ private final ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ private final OutputStreamWriter writer;
+ /** Instantiates a new MultipartSerializationWriter. */
+ public MultipartSerializationWriter() {
+ try {
+ this.writer = new OutputStreamWriter(this.stream, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("could not create writer", e);
+ }
+ }
+ public void writeStringValue(@Nullable final String key, @Nullable final String value) {
+ try {
+ if (key != null && !key.isEmpty())
+ writer.write(key);
+ if (value != null && !value.isEmpty()) {
+ if (key != null && !key.isEmpty())
+ writer.write(": ");
+ writer.write(value);
+ }
+ writer.write("\r\n");
+ writer.flush();
+ } catch (IOException ex) {
+ throw new RuntimeException("could not serialize value", ex);
+ }
+ }
+ public void writeBooleanValue(@Nullable final String key, @Nullable final Boolean value) {
+ throw new UnsupportedOperationException();
+ }
+ public void writeShortValue(@Nullable final String key, @Nullable final Short value) {
+ throw new UnsupportedOperationException();
+ }
+ public void writeByteValue(@Nullable final String key, @Nullable final Byte value) {
+ throw new UnsupportedOperationException();
+ }
+ public void writeBigDecimalValue(@Nullable final String key, @Nullable final BigDecimal value) {
+ throw new UnsupportedOperationException();
+ }
+ public void writeIntegerValue(@Nullable final String key, @Nullable final Integer value) {
+ throw new UnsupportedOperationException();
+ }
+ public void writeFloatValue(@Nullable final String key, @Nullable final Float value) {
+ throw new UnsupportedOperationException();
+ }
+ public void writeDoubleValue(@Nullable final String key, @Nullable final Double value) {
+ throw new UnsupportedOperationException();
+ }
+ public void writeLongValue(@Nullable final String key, @Nullable final Long value) {
+ throw new UnsupportedOperationException();
+ }
+ public void writeUUIDValue(@Nullable final String key, @Nullable final UUID value) {
+ throw new UnsupportedOperationException();
+ }
+ public void writeOffsetDateTimeValue(@Nullable final String key, @Nullable final OffsetDateTime value) {
+ throw new UnsupportedOperationException();
+ }
+ public void writeLocalDateValue(@Nullable final String key, @Nullable final LocalDate value) {
+ throw new UnsupportedOperationException();
+ }
+ public void writeLocalTimeValue(@Nullable final String key, @Nullable final LocalTime value) {
+ throw new UnsupportedOperationException();
+ }
+ public void writePeriodAndDurationValue(@Nullable final String key, @Nullable final PeriodAndDuration value) {
+ throw new UnsupportedOperationException();
+ }
+ public void writeCollectionOfPrimitiveValues(@Nullable final String key, @Nullable final Iterable values) {
+ throw new UnsupportedOperationException();
+ }
+ public void writeCollectionOfObjectValues(@Nullable final String key, @Nullable final Iterable values) {
+ throw new UnsupportedOperationException();
+ }
+ public > void writeCollectionOfEnumValues(@Nullable final String key, @Nullable final Iterable values) {
+ throw new UnsupportedOperationException();
+ }
+ public void writeObjectValue(@Nullable final String key, @Nullable final T value, @Nonnull final Parsable ...additionalValuesToMerge) {
+ Objects.requireNonNull(additionalValuesToMerge);
+ if(value != null) {
+ if(onBeforeObjectSerialization != null) {
+ onBeforeObjectSerialization.accept(value);
+ }
+ if(value instanceof MultipartBody) {
+ if(onStartObjectSerialization != null) {
+ onStartObjectSerialization.accept(value, this);
+ }
+ value.serialize(this);
+ } else {
+ throw new RuntimeException("expected MultipartBody instance but got " + value.getClass().getName());
+ }
+ if(onAfterObjectSerialization != null) {
+ onAfterObjectSerialization.accept(value);
+ }
+ }
+ }
+ public > void writeEnumSetValue(@Nullable final String key, @Nullable final EnumSet values) {
+ throw new UnsupportedOperationException();
+ }
+ public > void writeEnumValue(@Nullable final String key, @Nullable final T value) {
+ throw new UnsupportedOperationException();
+ }
+ public void writeNullValue(@Nullable final String key) {
+ throw new UnsupportedOperationException();
+ }
+ @Nonnull
+ public InputStream getSerializedContent() {
+ return new ByteArrayInputStream(this.stream.toByteArray());
+ }
+ public void close() throws IOException {
+ this.writer.close();
+ this.stream.close();
+ }
+ public void writeAdditionalData(@Nonnull final Map value) {
+ throw new UnsupportedOperationException();
+ }
+ @Nullable
+ public Consumer getOnBeforeObjectSerialization() {
+ return this.onBeforeObjectSerialization;
+ }
+ @Nullable
+ public Consumer getOnAfterObjectSerialization() {
+ return this.onAfterObjectSerialization;
+ }
+ @Nullable
+ public BiConsumer getOnStartObjectSerialization() {
+ return this.onStartObjectSerialization;
+ }
+ private Consumer onBeforeObjectSerialization;
+ public void setOnBeforeObjectSerialization(@Nullable final Consumer value) {
+ this.onBeforeObjectSerialization = value;
+ }
+ private Consumer onAfterObjectSerialization;
+ public void setOnAfterObjectSerialization(@Nullable final Consumer value) {
+ this.onAfterObjectSerialization = value;
+ }
+ private BiConsumer onStartObjectSerialization;
+ public void setOnStartObjectSerialization(@Nullable final BiConsumer value) {
+ this.onStartObjectSerialization = value;
+ }
+ public void writeByteArrayValue(@Nullable final String key, @Nullable @Nonnull final byte[] value) {
+ if(value != null)
+ try {
+ this.stream.write(value);
+ } catch (IOException ex) {
+ throw new RuntimeException("could not serialize value", ex);
+ }
+ }
+}
diff --git a/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriterFactory.java b/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriterFactory.java
new file mode 100644
index 000000000..6411d1fcc
--- /dev/null
+++ b/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriterFactory.java
@@ -0,0 +1,28 @@
+package com.microsoft.kiota.serialization;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+/** Creates instances of Multipart Serialization Writers */
+public class MultipartSerializationWriterFactory implements SerializationWriterFactory {
+ /** Instantiates a new factory */
+ public MultipartSerializationWriterFactory() {
+ }
+ @Nonnull
+ public String getValidContentType() {
+ return validContentType;
+ }
+ private static final String validContentType = "multipart/form-data";
+ @Override
+ @Nonnull
+ public SerializationWriter getSerializationWriter(@Nonnull final String contentType) {
+ Objects.requireNonNull(contentType, "parameter contentType cannot be null");
+ if(contentType.isEmpty()) {
+ throw new NullPointerException("contentType cannot be empty");
+ } else if (!contentType.equals(validContentType)) {
+ throw new IllegalArgumentException("expected a " + validContentType + " content type");
+ }
+ return new MultipartSerializationWriter();
+ }
+}
diff --git a/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java b/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java
new file mode 100644
index 000000000..88aa2eb7e
--- /dev/null
+++ b/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java
@@ -0,0 +1,89 @@
+package com.microsoft.kiota.serialization;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.OffsetDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+import com.microsoft.kiota.PeriodAndDuration;
+import com.microsoft.kiota.MultipartBody;
+import com.microsoft.kiota.RequestAdapter;
+import com.microsoft.kiota.serialization.JsonSerializationWriterFactory;
+import org.junit.jupiter.api.Test;
+
+import com.microsoft.kiota.serialization.mocks.TestEntity;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class MultipartSerializationWriterTests {
+ @Test
+ public void throwsOnParsable() throws IOException, UnsupportedEncodingException {
+ final var testEntity = new TestEntity();
+ testEntity.setId("48d31887-5fad-4d73-a9f5-3c356e68a038");
+ testEntity.setWorkDuration(PeriodAndDuration.parse("P1M"));
+ testEntity.setStartWorkTime(LocalTime.of(8, 0, 0));
+ testEntity.setBirthDay(LocalDate.of(2017, 9, 4));
+ testEntity.setDeviceNames(Arrays.asList("device1","device2"));
+ testEntity.getAdditionalData().put("mobilePhone", null);
+ testEntity.getAdditionalData().put("jobTitle", "Author");
+ testEntity.getAdditionalData().put("accountEnabled", false);
+ testEntity.getAdditionalData().put("createdDateTime", OffsetDateTime.MIN);
+ testEntity.getAdditionalData().put("otherPhones", Arrays.asList(Arrays.asList("device1","device2")));
+ try (final var serializationWriter = new MultipartSerializationWriter()) {
+ assertThrows(RuntimeException.class, () -> serializationWriter.writeObjectValue(null, testEntity));
+ }
+ }
+ private final byte[] byteForTest = new byte[] { 0x01, 0x02, 0x03 };
+ @Test
+ public void writesBytArrayValue() throws IOException {
+ try (final var serializationWriter = new MultipartSerializationWriter()) {
+ serializationWriter.writeByteArrayValue("key", byteForTest);
+ try(final var result = serializationWriter.getSerializedContent()) {
+ try(final var reader = new BufferedReader(new InputStreamReader(result, "UTF-8"))) {
+ final String strResult = reader.lines().collect(Collectors.joining("\n"));
+ assertEquals("\u0001\u0002\u0003", strResult);
+ }
+ }
+ }
+ }
+ @Test
+ public void writesAStructuredObject() throws IOException {
+ final TestEntity testEntity = new TestEntity();
+ testEntity.setId("48d31887-5fad-4d73-a9f5-3c356e68a038");
+ testEntity.setWorkDuration(PeriodAndDuration.parse("P1M"));
+ testEntity.setStartWorkTime(LocalTime.of(8, 0, 0));
+ testEntity.setBirthDay(LocalDate.of(2017, 9, 4));
+ testEntity.setDeviceNames(Arrays.asList("device1","device2"));
+ testEntity.getAdditionalData().put("mobilePhone", null);
+ testEntity.getAdditionalData().put("jobTitle", "Author");
+ testEntity.getAdditionalData().put("accountEnabled", false);
+ testEntity.getAdditionalData().put("createdDateTime", OffsetDateTime.MIN);
+ testEntity.getAdditionalData().put("otherPhones", Arrays.asList(Arrays.asList("device1","device2")));
+ final RequestAdapter requestAdapter = mock(RequestAdapter.class);
+ when(requestAdapter.getSerializationWriterFactory()).thenReturn(new JsonSerializationWriterFactory());
+ try (final var serializationWriter = new MultipartSerializationWriter()) {
+ final MultipartBody multipartBody = new MultipartBody();
+ multipartBody.requestAdapter = requestAdapter;
+ multipartBody.addOrReplacePart("testEntity", "application/json", testEntity);
+ multipartBody.addOrReplacePart("image", "application/octet-stream", byteForTest);
+ serializationWriter.writeObjectValue(null, multipartBody);
+
+ try(final var result = serializationWriter.getSerializedContent()) {
+ try(final var reader = new BufferedReader(new InputStreamReader(result, "UTF-8"))) {
+ final String strResult = reader.lines().collect(Collectors.joining("\r\n"));
+ assertEquals("--" + multipartBody.getBoundary() + "\r\nContent-Type: application/octet-stream\r\nContent-Disposition: form-data; name=\"image\"\r\n\r\n"+new String(byteForTest, "UTF-8")+"\r\n--" + multipartBody.getBoundary() + "\r\nContent-Type: application/json\r\nContent-Disposition: form-data; name=\"testEntity\"\r\n\r\n{\"id\":\"48d31887-5fad-4d73-a9f5-3c356e68a038\",\"birthDay\":\"2017-09-04\",\"workDuration\":\"P1M\",\"startWorkTime\":\"08:00:00\",\"deviceNames\":[\"device1\",\"device2\"],\"mobilePhone\":null,\"jobTitle\":\"Author\",\"createdDateTime\":\"-999999999-01-01T00:00:00+18:00\",\"otherPhones\":[[\"device1\",\"device2\"]],\"accountEnabled\":false}\r\n--" + multipartBody.getBoundary() + "--", strResult);
+ }
+ }
+ }
+ }
+}
diff --git a/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/SerializationWriterFactoryTests.java b/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/SerializationWriterFactoryTests.java
new file mode 100644
index 000000000..73e918f0f
--- /dev/null
+++ b/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/SerializationWriterFactoryTests.java
@@ -0,0 +1,24 @@
+package com.microsoft.kiota.serialization;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+public class SerializationWriterFactoryTests {
+ private static final MultipartSerializationWriterFactory _serializationWriterFactory = new MultipartSerializationWriterFactory();
+ private static final String contentType = "multipart/form-data";
+ @Test
+ public void getsWriterForMultipartContentType() {
+ final var serializationWriter = _serializationWriterFactory.getSerializationWriter(contentType);
+ assertNotNull(serializationWriter);
+ }
+ @Test
+ public void throwsArgumentOutOfRangeExceptionForInvalidContentType() {
+ assertThrows(IllegalArgumentException.class, () -> _serializationWriterFactory.getSerializationWriter("application/json"));
+ }
+ @Test
+ public void throwsArgumentNullExceptionForNoContentType() {
+ assertThrows(NullPointerException.class, () -> _serializationWriterFactory.getSerializationWriter(""));
+ }
+}
diff --git a/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/mocks/SecondTestEntity.java b/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/mocks/SecondTestEntity.java
new file mode 100644
index 000000000..71793aa62
--- /dev/null
+++ b/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/mocks/SecondTestEntity.java
@@ -0,0 +1,70 @@
+package com.microsoft.kiota.serialization.mocks;
+
+import com.microsoft.kiota.serialization.ParseNode;
+import com.microsoft.kiota.serialization.SerializationWriter;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import com.microsoft.kiota.serialization.Parsable;
+import com.microsoft.kiota.serialization.AdditionalDataHolder;
+
+public class SecondTestEntity implements Parsable, AdditionalDataHolder {
+ private String _displayName;
+ private Integer _id;
+ private Long _failureRate;
+
+ @Override
+ public Map> getFieldDeserializers() {
+ return new HashMap<>(){{
+ put("displayName", (n) -> setDisplayName(n.getStringValue()));
+ put("id", (n) -> setId(n.getIntegerValue()));
+ put("failureRate", (n) -> setFailureRate(n.getLongValue()));
+ }};
+ }
+
+ @Override
+ public void serialize(SerializationWriter writer) {
+ Objects.requireNonNull(writer);
+
+ writer.writeStringValue("displayName", getDisplayName());
+ writer.writeIntegerValue("id", getId());
+ writer.writeLongValue("failureRate", getFailureRate());
+ writer.writeAdditionalData(getAdditionalData());
+ }
+ private final Map _additionalData = new HashMap<>();
+ @Override
+ public Map getAdditionalData() {
+ return _additionalData;
+ }
+
+ public String getDisplayName() {
+ return _displayName;
+ }
+
+ public void setDisplayName(String value) {
+ this._displayName = value;
+ }
+
+ public Integer getId() {
+ return _id;
+ }
+
+ public void setId(Integer value) {
+ this._id = value;
+ }
+
+ public Long getFailureRate() {
+ return _failureRate;
+ }
+
+ public void setFailureRate(Long value) {
+ this._failureRate = value;
+ }
+ @javax.annotation.Nonnull
+ public static SecondTestEntity createFromDiscriminatorValue(@javax.annotation.Nonnull final ParseNode parseNode) {
+ return new SecondTestEntity();
+ }
+}
diff --git a/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/mocks/TestEntity.java b/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/mocks/TestEntity.java
new file mode 100644
index 000000000..e2eefbc07
--- /dev/null
+++ b/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/mocks/TestEntity.java
@@ -0,0 +1,144 @@
+package com.microsoft.kiota.serialization.mocks;
+
+import com.microsoft.kiota.PeriodAndDuration;
+import com.microsoft.kiota.serialization.ParseNode;
+import com.microsoft.kiota.serialization.SerializationWriter;
+
+import java.util.*;
+import java.util.function.Consumer;
+import java.time.OffsetDateTime;
+import java.time.LocalDate;
+import java.time.LocalTime;
+
+import com.microsoft.kiota.serialization.Parsable;
+import com.microsoft.kiota.serialization.AdditionalDataHolder;
+
+public class TestEntity implements Parsable, AdditionalDataHolder {
+ private String _id;
+ public String getId() {
+ return _id;
+ }
+
+ public void setId(String _id) {
+ this._id = _id;
+ }
+
+ private String _officeLocation;
+ public String getOfficeLocation() {
+ return _officeLocation;
+ }
+
+ public void setOfficeLocation(String _officeLocation) {
+ this._officeLocation = _officeLocation;
+ }
+
+ private LocalDate _birthDay;
+ public LocalDate getBirthDay() {
+ return _birthDay;
+ }
+
+ public void setBirthDay(LocalDate value) {
+ this._birthDay = value;
+ }
+
+ private List _deviceNames;
+ public List getDeviceNames() {
+ return _deviceNames;
+ }
+
+ public void setDeviceNames(List value) {
+ this._deviceNames = new ArrayList(value);
+ }
+ private PeriodAndDuration _workDuration;
+ public PeriodAndDuration getWorkDuration() {
+ return _workDuration;
+ }
+
+ public void setWorkDuration(PeriodAndDuration value) {
+ this._workDuration = value;
+ }
+
+ private LocalTime _startWorkTime;
+ public LocalTime getStartWorkTime() {
+ return _startWorkTime;
+ }
+
+ public void setStartWorkTime(LocalTime value) {
+ this._startWorkTime = value;
+ }
+
+ private LocalTime _endWorkTime;
+ public LocalTime getEndWorkTime() {
+ return _endWorkTime;
+ }
+
+ public void setEndWorkTime(LocalTime value) {
+ this._endWorkTime = value;
+ }
+
+ //TODO enum
+ private OffsetDateTime _createdDateTime;
+
+ public OffsetDateTime getCreatedDateTime() {
+ return _createdDateTime;
+ }
+
+ public void setCreatedDateTime(OffsetDateTime value) {
+ this._createdDateTime = value;
+ }
+
+ @Override
+ public Map> getFieldDeserializers() {
+ return new HashMap<>() {{
+ put("id", (n) -> {
+ setId(n.getStringValue());
+ });
+ put("officeLocation", (n) -> {
+ setOfficeLocation(n.getStringValue());
+ });
+ put("birthDay", (n) -> {
+ setBirthDay(n.getLocalDateValue());
+ });
+ put("workDuration", (n) -> {
+ setWorkDuration(n.getPeriodAndDurationValue());
+ });
+ put("startWorkTime", (n) -> {
+ setStartWorkTime(n.getLocalTimeValue());
+ });
+ put("endWorkTime", (n) -> {
+ setEndWorkTime(n.getLocalTimeValue());
+ });
+ put("createdDateTime", (n) -> {
+ setCreatedDateTime(n.getOffsetDateTimeValue());
+ });
+ put("deviceNames", (n) -> {
+ setDeviceNames(n.getCollectionOfPrimitiveValues(String.class));
+ });
+ }};
+ }
+
+ @Override
+ public void serialize(SerializationWriter writer) {
+ Objects.requireNonNull(writer);
+ writer.writeStringValue("id", getId());
+ writer.writeStringValue("officeLocation", getOfficeLocation());
+ writer.writeLocalDateValue("birthDay", getBirthDay());
+ writer.writePeriodAndDurationValue("workDuration", getWorkDuration());
+ writer.writeLocalTimeValue("startWorkTime", getStartWorkTime());
+ writer.writeLocalTimeValue("endWorkTime", getEndWorkTime());
+ writer.writeOffsetDateTimeValue("createdDateTime", getCreatedDateTime());
+ writer.writeCollectionOfPrimitiveValues("deviceNames", getDeviceNames());
+ writer.writeAdditionalData(getAdditionalData());
+ }
+
+ private final Map _additionalData = new HashMap<>();
+
+ @Override
+ public Map getAdditionalData() {
+ return _additionalData;
+ }
+ @javax.annotation.Nonnull
+ public static TestEntity createFromDiscriminatorValue(@javax.annotation.Nonnull final ParseNode parseNode) {
+ return new TestEntity();
+ }
+}
diff --git a/gradle.properties b/gradle.properties
index 6a1346055..be6ba7aef 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -25,8 +25,8 @@ org.gradle.caching=true
mavenGroupId = com.microsoft.kiota
mavenMajorVersion = 0
-mavenMinorVersion = 4
-mavenPatchVersion = 7
+mavenMinorVersion = 5
+mavenPatchVersion = 0
mavenArtifactSuffix =
#These values are used to run functional tests
diff --git a/settings.gradle b/settings.gradle
index f9579ac0d..bff5bcbbc 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -10,5 +10,6 @@ include ':components:abstractions' + suffix
include ':components:serialization:form' + suffix
include ':components:serialization:json' + suffix
include ':components:serialization:text' + suffix
+include ':components:serialization:multipart' + suffix
include ':components:authentication:azure' + suffix
include ':components:http:okHttp' + suffix