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

feature/multipart #517

Merged
merged 11 commits into from
Aug 2, 2023
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: ./
Expand Down Expand Up @@ -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: ./
Expand All @@ -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: ./
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 25 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand All @@ -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:
Expand All @@ -36,32 +38,37 @@ In `pom.xml` in the `dependencies` section:
<dependency>
<groupId>com.microsoft.kiota</groupId>
<artifactId>microsoft-kiota-abstractions</artifactId>
<version>0.4.0</version>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.kiota</groupId>
<artifactId>microsoft-kiota-authentication-azure</artifactId>
<version>0.4.0</version>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.kiota</groupId>
<artifactId>microsoft-kiota-http-okHttp</artifactId>
<version>0.4.0</version>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.kiota</groupId>
<artifactId>microsoft-kiota-serialization-json</artifactId>
<version>0.4.0</version>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.kiota</groupId>
<artifactId>microsoft-kiota-serialization-text</artifactId>
<version>0.4.0</version>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.kiota</groupId>
<artifactId>microsoft-kiota-serialization-form</artifactId>
<version>0.4.0</version>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.kiota</groupId>
<artifactId>microsoft-kiota-serialization-multipart</artifactId>
<version>0.5.0</version>
</dependency>
```

Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T> 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 <T> 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<String, Map.Entry<String, Object>> parts = new HashMap<>();
private final Map<String, String> 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<String, Object> 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<String, Consumer<ParseNode>> 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<String, Map.Entry<String, Object>> 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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,13 @@ public <T extends Parsable> 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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Loading