diff --git a/README.md b/README.md
index 7c283b9..012596c 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/feign-form-spring/src/main/java/feign/form/spring/converter/SpringMultipartMixedReader.java b/feign-form-spring/src/main/java/feign/form/spring/converter/SpringMultipartMixedReader.java
new file mode 100644
index 0000000..4d9f4ea
--- /dev/null
+++ b/feign-form-spring/src/main/java/feign/form/spring/converter/SpringMultipartMixedReader.java
@@ -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}).
+ *
+ * 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 {
+
+ 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();
+ 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 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);
+ }
+}
diff --git a/feign-form-spring/src/test/java/feign/form/feign/spring/DownloadClient.java b/feign-form-spring/src/test/java/feign/form/feign/spring/DownloadClient.java
index 4d1be65..45207d2 100644
--- a/feign-form-spring/src/test/java/feign/form/feign/spring/DownloadClient.java
+++ b/feign-form-spring/src/test/java/feign/form/feign/spring/DownloadClient.java
@@ -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 {
diff --git a/feign-form-spring/src/test/java/feign/form/feign/spring/SpringMultipartDecoderTest.java b/feign-form-spring/src/test/java/feign/form/feign/spring/SpringMultipartDecoderTest.java
index b474f7d..95776f6 100644
--- a/feign-form-spring/src/test/java/feign/form/feign/spring/SpringMultipartDecoderTest.java
+++ b/feign-form-spring/src/test/java/feign/form/feign/spring/SpringMultipartDecoderTest.java
@@ -33,7 +33,7 @@
webEnvironment = DEFINED_PORT,
classes = Server.class,
properties = {
- "server.port=8081",
+ "server.port=8082",
"feign.hystrix.enabled=false"
}
)