Skip to content
This repository has been archived by the owner on Dec 31, 2024. It is now read-only.

added multipart/mixed support #104

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,10 @@ public interface DownloadClient {
}
}
```
### multipart/mixed
Replace `SpringManyMultipartFilesReader` in the `DownloadClient` example above with `SpringMultipartMixedReader`.

#### TODO
- update the maven dependency versions, they are out of date
- refactor a common SpringMultiXXX base class with the common parts of the two Spring readers
- increment the versions -> 3.8.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package feign.form.spring.converter;

import static feign.form.util.CharsetUtil.UTF_8;
import static lombok.AccessLevel.PRIVATE;
import static org.springframework.http.HttpHeaders.CONTENT_DISPOSITION;
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.LinkedList;
import java.util.Map;
import java.util.regex.Pattern;

import lombok.experimental.FieldDefaults;
import lombok.val;
import org.apache.commons.fileupload.MultipartStream;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

/**
* Implementation of {@link HttpMessageConverter} that can read multipart/form-data HTTP bodies
* (writing is not handled because that is already supported by {@link FormHttpMessageConverter}).
* <p>
* This reader supports an array of {@link MultipartFile} as the mapping return class type - each
* multipart body is read into an underlying byte array (in memory) implemented via
* {@link ByteArrayMultipartFile}.
*/
@FieldDefaults(level = PRIVATE, makeFinal = true)
public class SpringMultipartMixedReader extends AbstractHttpMessageConverter<MultipartFile[]> {

private static final Pattern NEWLINES_PATTERN = Pattern.compile("\\R");

private static final Pattern COLON_PATTERN = Pattern.compile(":");

private static final Pattern SEMICOLON_PATTERN = Pattern.compile(";");

private static final Pattern EQUALITY_SIGN_PATTERN = Pattern.compile("=");

int bufSize;

/**
* Construct an {@code AbstractHttpMessageConverter} that can read mulitpart/form-data.
*
* @param bufSize The size of the buffer (in bytes) to read the HTTP multipart body.
*/
public SpringMultipartMixedReader (int bufSize) {
super(new MediaType("multipart", "mixed")); // TODO later version of Spring MVC MediaType has this
this.bufSize = bufSize;
}

@Override
protected boolean canWrite (MediaType mediaType) {
return false; // Class NOT meant for writing multipart/form-data HTTP bodies
}

@Override
protected boolean supports (Class<?> clazz) {
return MultipartFile[].class == clazz;
}

@Override
protected MultipartFile[] readInternal (Class<? extends MultipartFile[]> clazz, HttpInputMessage inputMessage
) throws IOException {
val headers = inputMessage.getHeaders();

MediaType contentType = headers.getContentType();
if (contentType == null) {
throw new HttpMessageNotReadableException("Content-Type is missing.", inputMessage);
}

val boundaryBytes = getMultiPartBoundary(contentType);
MultipartStream multipartStream = new MultipartStream(inputMessage.getBody(), boundaryBytes, bufSize, null);

val multiparts = new LinkedList<ByteArrayMultipartFile>();
for (boolean nextPart = multipartStream.skipPreamble(); nextPart; nextPart = multipartStream.readBoundary()) {
ByteArrayMultipartFile multiPart;
try {
multiPart = readMultiPart(multipartStream);
} catch (Exception e) {
throw new HttpMessageNotReadableException("Multipart body could not be read.", e, inputMessage);
}
multiparts.add(multiPart);
}
return multiparts.toArray(new ByteArrayMultipartFile[0]);
}

@Override
protected void writeInternal (MultipartFile[] byteArrayMultipartFiles, HttpOutputMessage outputMessage) {
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not support writing to HTTP body.");
}

private byte[] getMultiPartBoundary (MediaType contentType) {
val boundaryString = unquote(contentType.getParameter("boundary"));
if (StringUtils.isEmpty(boundaryString)) {
throw new HttpMessageConversionException("Content-Type missing boundary information.");
}
return boundaryString.getBytes(UTF_8);
}

private ByteArrayMultipartFile readMultiPart (MultipartStream multipartStream) throws IOException {
val multiPartHeaders = splitIntoKeyValuePairs(
multipartStream.readHeaders(),
NEWLINES_PATTERN,
COLON_PATTERN,
false
);

val contentDisposition = splitIntoKeyValuePairs(
multiPartHeaders.get(CONTENT_DISPOSITION),
SEMICOLON_PATTERN,
EQUALITY_SIGN_PATTERN,
true
);

val bodyStream = new ByteArrayOutputStream();
multipartStream.readBodyData(bodyStream);
return new ByteArrayMultipartFile(
contentDisposition.get("name"),
contentDisposition.get("filename"),
multiPartHeaders.get(CONTENT_TYPE),
bodyStream.toByteArray()
);
}

private Map<String, String> splitIntoKeyValuePairs (String str, Pattern entriesSeparatorPattern,
Pattern keyValueSeparatorPattern, boolean unquoteValue
) {
val keyValuePairs = new IgnoreKeyCaseMap();
if (!StringUtils.isEmpty(str)) {
val tokens = entriesSeparatorPattern.split(str);
for (val token : tokens) {
val pair = keyValueSeparatorPattern.split(token.trim(), 2);
val key = pair[0].trim();
val value = pair.length > 1
? pair[1].trim()
: "";

keyValuePairs.put(key, unquoteValue
? unquote(value)
: value);
}
}
return keyValuePairs;
}

private String unquote (String value) {
if (value == null) {
return null;
}
return isSurroundedBy(value, "\"") || isSurroundedBy(value, "'")
? value.substring(1, value.length() - 1)
: value;
}

private boolean isSurroundedBy (String value, String preSuffix) {
return value.length() > 1 && value.startsWith(preSuffix) && value.endsWith(preSuffix);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

@FeignClient(
name = "multipart-download-support-service",
url = "http://localhost:8081",
url = "http://localhost:8082",
configuration = DownloadClient.ClientConfiguration.class
)
public interface DownloadClient {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
webEnvironment = DEFINED_PORT,
classes = Server.class,
properties = {
"server.port=8081",
"server.port=8082",
"feign.hystrix.enabled=false"
}
)
Expand Down