Skip to content

Commit

Permalink
Merge pull request #1150 from piotrooo/feature/introduce-multipartbod…
Browse files Browse the repository at this point in the history
…y-filename-directive

Introduce MultipartBody filename directive
  • Loading branch information
andrueastman authored Apr 4, 2024
2 parents a7a6569 + 97da359 commit d24eb36
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 16 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.1.4] - 2024-04-04

### Added

- Introduces a `filename` directive in the `MultipartBody`.

### Changed

- Replaces `@Nullable` annotations to `@Nonnull` in the `BaseRequestConfiguration`.
Expand Down
4 changes: 4 additions & 0 deletions components/abstractions/spotBugsExcludeFilter.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,8 @@ xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubu
<Bug pattern="NP_LOAD_OF_KNOWN_NULL_VALUE" />
<Class name="com.microsoft.kiota.store.InMemoryBackingStore" />
</Match>
<Match>
<Bug pattern="SIC_INNER_SHOULD_BE_STATIC" />
<Class name="com.microsoft.kiota.MultipartBody$Part" />
</Match>
</FindBugsFilter>
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,35 @@ public MultipartBody() {
*/
public <T> void addOrReplacePart(
@Nonnull final String name, @Nonnull final String contentType, @Nonnull final T value) {
addOrReplacePart(name, contentType, value, null);
}

/**
* 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.
* @param filename the value of the filename directive.
*/
public <T> void addOrReplacePart(
@Nonnull final String name,
@Nonnull final String contentType,
@Nonnull final T value,
@Nullable String filename) {
Objects.requireNonNull(value);
if (Compatibility.isBlank(contentType))
throw new IllegalArgumentException("contentType cannot be blank or empty");
if (Compatibility.isBlank(name))
throw new IllegalArgumentException("name cannot be blank or empty");

final String normalizedName = normalizePartName(name);
originalNames.put(normalizedName, name);
parts.put(normalizedName, new AbstractMap.SimpleEntry<>(contentType, value));
Part part = new Part(name, value, contentType, filename);
parts.put(normalizedName, part);
}

private final Map<String, Map.Entry<String, Object>> parts = new HashMap<>();
private final Map<String, String> originalNames = new HashMap<>();
private final Map<String, Part> parts = new HashMap<>();

private String normalizePartName(@Nonnull final String original) {
return original.toLowerCase(Locale.ROOT);
Expand All @@ -75,7 +91,7 @@ private String normalizePartName(@Nonnull final String original) {
if (Compatibility.isBlank(partName))
throw new IllegalArgumentException("partName cannot be blank or empty");
final String normalizedName = normalizePartName(partName);
final Map.Entry<String, Object> candidate = parts.get(normalizedName);
final Part candidate = parts.get(normalizedName);
if (candidate == null) return null;
return candidate.getValue();
}
Expand All @@ -90,10 +106,7 @@ public boolean removePart(@Nonnull final String partName) {
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;
return candidate != null;
}

/** {@inheritDoc} */
Expand All @@ -111,18 +124,23 @@ public void serialize(@Nonnull final SerializationWriter writer) {
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()) {
for (final Map.Entry<String, Part> partEntry : parts.entrySet()) {
try {
Part part = partEntry.getValue();
if (isFirst) isFirst = false;
else writer.writeStringValue("", "");
writer.writeStringValue("", "--" + getBoundary());
final String partContentType = partEntry.getValue().getKey();
final String partContentType = part.getContentType();
writer.writeStringValue("Content-Type", partContentType);
writer.writeStringValue(
"Content-Disposition",
"form-data; name=\"" + originalNames.get(partEntry.getKey()) + "\"");

String contentDisposition = "form-data; name=\"" + part.getName() + "\"";
if (part.getFilename() != null && !part.getFilename().trim().isEmpty()) {
contentDisposition += "; filename=\"" + part.getFilename() + "\"";
}
writer.writeStringValue("Content-Disposition", contentDisposition);

writer.writeStringValue("", "");
final Object objectValue = partEntry.getValue().getValue();
final Object objectValue = part.getValue();
if (objectValue instanceof Parsable) {
try (final SerializationWriter partWriter =
serializationFactory.getSerializationWriter(partContentType)) {
Expand Down Expand Up @@ -151,4 +169,34 @@ public void serialize(@Nonnull final SerializationWriter writer) {
writer.writeStringValue("", "");
writer.writeStringValue("", "--" + boundary + "--");
}

private class Part {
private final String name;
private final Object value;
private final String contentType;
private final String filename;

Part(String name, Object value, String contentType, String filename) {
this.name = name;
this.value = value;
this.contentType = contentType;
this.filename = filename;
}

public String getName() {
return name;
}

public Object getValue() {
return value;
}

public String getContentType() {
return contentType;
}

public String getFilename() {
return filename;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import com.microsoft.kiota.serialization.SerializationWriter;

Expand Down Expand Up @@ -67,5 +68,27 @@ void removesPart() {
final Object result = multipartBody.getPartValue("foo");
assertNull(result);
}

@Test
void notAddFilename() {
final MultipartBody multipartBody = new MultipartBody();
final SerializationWriter writer = mock(SerializationWriter.class);
multipartBody.requestAdapter = mock(RequestAdapter.class);
multipartBody.addOrReplacePart("foo", "bar", "baz");
multipartBody.serialize(writer);
verify(writer).writeStringValue("Content-Disposition", "form-data; name=\"foo\"");
}

@Test
void addFilename() {
final MultipartBody multipartBody = new MultipartBody();
final SerializationWriter writer = mock(SerializationWriter.class);
multipartBody.requestAdapter = mock(RequestAdapter.class);
multipartBody.addOrReplacePart("foo", "bar", "baz", "image.png");
multipartBody.serialize(writer);
verify(writer)
.writeStringValue(
"Content-Disposition", "form-data; name=\"foo\"; filename=\"image.png\"");
}
// serialize method is being tested in the serialization library
}
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ org.gradle.caching=true
mavenGroupId = com.microsoft.kiota
mavenMajorVersion = 1
mavenMinorVersion = 1
mavenPatchVersion = 3
mavenPatchVersion = 4
mavenArtifactSuffix =

#These values are used to run functional tests
Expand Down

0 comments on commit d24eb36

Please sign in to comment.