001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.http.client;
018
019import java.util.Arrays;
020import java.util.List;
021import java.util.Map;
022import java.util.function.Consumer;
023
024import org.reactivestreams.Publisher;
025
026import org.springframework.core.ParameterizedTypeReference;
027import org.springframework.core.ResolvableType;
028import org.springframework.core.ResolvableTypeProvider;
029import org.springframework.core.io.buffer.DataBuffer;
030import org.springframework.http.HttpEntity;
031import org.springframework.http.HttpHeaders;
032import org.springframework.http.MediaType;
033import org.springframework.http.codec.multipart.FilePart;
034import org.springframework.http.codec.multipart.Part;
035import org.springframework.lang.NonNull;
036import org.springframework.lang.Nullable;
037import org.springframework.util.Assert;
038import org.springframework.util.LinkedMultiValueMap;
039import org.springframework.util.MultiValueMap;
040
041/**
042 * Prepare the body of a multipart request, resulting in a
043 * {@code MultiValueMap<String, HttpEntity>}. Parts may be concrete values or
044 * via asynchronous types such as Reactor {@code Mono}, {@code Flux}, and
045 * others registered in the
046 * {@link org.springframework.core.ReactiveAdapterRegistry ReactiveAdapterRegistry}.
047 *
048 * <p>This builder is intended for use with the reactive
049 * {@link org.springframework.web.reactive.function.client.WebClient WebClient}.
050 * For multipart requests with the {@code RestTemplate}, simply create and
051 * populate a {@code MultiValueMap<String, HttpEntity>} as shown in the Javadoc for
052 * {@link org.springframework.http.converter.FormHttpMessageConverter FormHttpMessageConverter}
053 * and in the
054 * <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#rest-template-multipart">reference docs</a>.
055 *
056 * <p>Below are examples of using this builder:
057 * <pre class="code">
058 *
059 * // Add form field
060 * MultipartBodyBuilder builder = new MultipartBodyBuilder();
061 * builder.part("form field", "form value").header("foo", "bar");
062 *
063 * // Add file part
064 * Resource image = new ClassPathResource("image.jpg");
065 * builder.part("image", image).header("foo", "bar");
066 *
067 * // Add content (e.g. JSON)
068 * Account account = ...
069 * builder.part("account", account).header("foo", "bar");
070 *
071 * // Add content from Publisher
072 * Mono&lt;Account&gt; accountMono = ...
073 * builder.asyncPart("account", accountMono).header("foo", "bar");
074 *
075 * // Build and use
076 * MultiValueMap&lt;String, HttpEntity&lt;?&gt;&gt; multipartBody = builder.build();
077 *
078 * Mono&lt;Void&gt; result = webClient.post()
079 *     .uri("...")
080 *     .body(multipartBody)
081 *     .retrieve()
082 *     .bodyToMono(Void.class)
083 * </pre>
084 *
085 * @author Arjen Poutsma
086 * @author Rossen Stoyanchev
087 * @since 5.0.2
088 * @see <a href="https://tools.ietf.org/html/rfc7578">RFC 7578</a>
089 */
090public final class MultipartBodyBuilder {
091
092        private final LinkedMultiValueMap<String, DefaultPartBuilder> parts = new LinkedMultiValueMap<>();
093
094
095        /**
096         * Creates a new, empty instance of the {@code MultipartBodyBuilder}.
097         */
098        public MultipartBodyBuilder() {
099        }
100
101
102        /**
103         * Add a part where the Object may be:
104         * <ul>
105         * <li>String -- form field
106         * <li>{@link org.springframework.core.io.Resource Resource} -- file part
107         * <li>Object -- content to be encoded (e.g. to JSON)
108         * <li>{@link HttpEntity} -- part content and headers although generally it's
109         * easier to add headers through the returned builder
110         * <li>{@link Part} -- a part from a server request
111         * </ul>
112         * @param name the name of the part to add
113         * @param part the part data
114         * @return builder that allows for further customization of part headers
115         */
116        public PartBuilder part(String name, Object part) {
117                return part(name, part, null);
118        }
119
120        /**
121         * Variant of {@link #part(String, Object)} that also accepts a MediaType.
122         * @param name the name of the part to add
123         * @param part the part data
124         * @param contentType the media type to help with encoding the part
125         * @return builder that allows for further customization of part headers
126         */
127        public PartBuilder part(String name, Object part, @Nullable MediaType contentType) {
128                Assert.hasLength(name, "'name' must not be empty");
129                Assert.notNull(part, "'part' must not be null");
130
131                if (part instanceof Part) {
132                        PartBuilder builder = asyncPart(name, ((Part) part).content(), DataBuffer.class);
133                        if (contentType != null) {
134                                builder.contentType(contentType);
135                        }
136                        if (part instanceof FilePart) {
137                                builder.filename(((FilePart) part).filename());
138                        }
139                        return builder;
140                }
141
142                if (part instanceof PublisherEntity<?,?>) {
143                        PublisherPartBuilder<?, ?> builder = new PublisherPartBuilder<>(name, (PublisherEntity<?, ?>) part);
144                        if (contentType != null) {
145                                builder.contentType(contentType);
146                        }
147                        this.parts.add(name, builder);
148                        return builder;
149                }
150
151                Object partBody;
152                HttpHeaders partHeaders = null;
153                if (part instanceof HttpEntity) {
154                        partBody = ((HttpEntity<?>) part).getBody();
155                        partHeaders = new HttpHeaders();
156                        partHeaders.putAll(((HttpEntity<?>) part).getHeaders());
157                }
158                else {
159                        partBody = part;
160                }
161
162                if (partBody instanceof Publisher) {
163                        throw new IllegalArgumentException(
164                                        "Use asyncPart(String, Publisher, Class)" +
165                                                        " or asyncPart(String, Publisher, ParameterizedTypeReference) or" +
166                                                        " or MultipartBodyBuilder.PublisherEntity");
167                }
168
169                DefaultPartBuilder builder = new DefaultPartBuilder(name, partHeaders, partBody);
170                if (contentType != null) {
171                        builder.contentType(contentType);
172                }
173                this.parts.add(name, builder);
174                return builder;
175        }
176
177        /**
178         * Add a part from {@link Publisher} content.
179         * @param name the name of the part to add
180         * @param publisher a Publisher of content for the part
181         * @param elementClass the type of elements contained in the publisher
182         * @return builder that allows for further customization of part headers
183         */
184        public <T, P extends Publisher<T>> PartBuilder asyncPart(String name, P publisher, Class<T> elementClass) {
185                Assert.hasLength(name, "'name' must not be empty");
186                Assert.notNull(publisher, "'publisher' must not be null");
187                Assert.notNull(elementClass, "'elementClass' must not be null");
188
189                PublisherPartBuilder<T, P> builder = new PublisherPartBuilder<>(name, null, publisher, elementClass);
190                this.parts.add(name, builder);
191                return builder;
192        }
193
194        /**
195         * Variant of {@link #asyncPart(String, Publisher, Class)} with a
196         * {@link ParameterizedTypeReference} for the element type information.
197         * @param name the name of the part to add
198         * @param publisher the part contents
199         * @param typeReference the type of elements contained in the publisher
200         * @return builder that allows for further customization of part headers
201         */
202        public <T, P extends Publisher<T>> PartBuilder asyncPart(
203                        String name, P publisher, ParameterizedTypeReference<T> typeReference) {
204
205                Assert.hasLength(name, "'name' must not be empty");
206                Assert.notNull(publisher, "'publisher' must not be null");
207                Assert.notNull(typeReference, "'typeReference' must not be null");
208
209                PublisherPartBuilder<T, P> builder = new PublisherPartBuilder<>(name, null, publisher, typeReference);
210                this.parts.add(name, builder);
211                return builder;
212        }
213
214        /**
215         * Return a {@code MultiValueMap} with the configured parts.
216         */
217        public MultiValueMap<String, HttpEntity<?>> build() {
218                MultiValueMap<String, HttpEntity<?>> result = new LinkedMultiValueMap<>(this.parts.size());
219                for (Map.Entry<String, List<DefaultPartBuilder>> entry : this.parts.entrySet()) {
220                        for (DefaultPartBuilder builder : entry.getValue()) {
221                                HttpEntity<?> entity = builder.build();
222                                result.add(entry.getKey(), entity);
223                        }
224                }
225                return result;
226        }
227
228
229        /**
230         * Builder that allows for further customization of part headers.
231         */
232        public interface PartBuilder {
233
234                /**
235                 * Set the {@linkplain MediaType media type} of the part.
236                 * @param contentType the content type
237                 * @since 5.2
238                 * @see HttpHeaders#setContentType(MediaType)
239                 */
240                PartBuilder contentType(MediaType contentType);
241
242                /**
243                 * Set the filename parameter for a file part. This should not be
244                 * necessary with {@link org.springframework.core.io.Resource Resource}
245                 * based parts that expose a filename but may be useful for
246                 * {@link Publisher} parts.
247                 * @param filename the filename to set on the Content-Disposition
248                 * @since 5.2
249                 */
250                PartBuilder filename(String filename);
251
252                /**
253                 * Add part header values.
254                 * @param headerName the part header name
255                 * @param headerValues the part header value(s)
256                 * @return this builder
257                 * @see HttpHeaders#addAll(String, List)
258                 */
259                PartBuilder header(String headerName, String... headerValues);
260
261                /**
262                 * Manipulate the part headers through the given consumer.
263                 * @param headersConsumer consumer to manipulate the part headers with
264                 * @return this builder
265                 */
266                PartBuilder headers(Consumer<HttpHeaders> headersConsumer);
267        }
268
269
270        private static class DefaultPartBuilder implements PartBuilder {
271
272                private final String name;
273
274                @Nullable
275                protected HttpHeaders headers;
276
277                @Nullable
278                protected final Object body;
279
280                public DefaultPartBuilder(String name, @Nullable HttpHeaders headers, @Nullable Object body) {
281                        this.name = name;
282                        this.headers = headers;
283                        this.body = body;
284                }
285
286                @Override
287                public PartBuilder contentType(MediaType contentType) {
288                        initHeadersIfNecessary().setContentType(contentType);
289                        return this;
290                }
291
292                @Override
293                public PartBuilder filename(String filename) {
294                        initHeadersIfNecessary().setContentDispositionFormData(this.name, filename);
295                        return this;
296                }
297
298                @Override
299                public PartBuilder header(String headerName, String... headerValues) {
300                        initHeadersIfNecessary().addAll(headerName, Arrays.asList(headerValues));
301                        return this;
302                }
303
304                @Override
305                public PartBuilder headers(Consumer<HttpHeaders> headersConsumer) {
306                        headersConsumer.accept(initHeadersIfNecessary());
307                        return this;
308                }
309
310                private HttpHeaders initHeadersIfNecessary() {
311                        if (this.headers == null) {
312                                this.headers = new HttpHeaders();
313                        }
314                        return this.headers;
315                }
316
317                public HttpEntity<?> build() {
318                        return new HttpEntity<>(this.body, this.headers);
319                }
320        }
321
322
323        private static class PublisherPartBuilder<S, P extends Publisher<S>> extends DefaultPartBuilder {
324
325                private final ResolvableType resolvableType;
326
327                public PublisherPartBuilder(String name, @Nullable HttpHeaders headers, P body, Class<S> elementClass) {
328                        super(name, headers, body);
329                        this.resolvableType = ResolvableType.forClass(elementClass);
330                }
331
332                public PublisherPartBuilder(String name, @Nullable HttpHeaders headers, P body,
333                                ParameterizedTypeReference<S> typeRef) {
334
335                        super(name, headers, body);
336                        this.resolvableType = ResolvableType.forType(typeRef);
337                }
338
339                public PublisherPartBuilder(String name, PublisherEntity<S, P> other) {
340                        super(name, other.getHeaders(), other.getBody());
341                        this.resolvableType = other.getResolvableType();
342                }
343
344                @Override
345                @SuppressWarnings("unchecked")
346                public HttpEntity<?> build() {
347                        P publisher = (P) this.body;
348                        Assert.state(publisher != null, "Publisher must not be null");
349                        return new PublisherEntity<>(this.headers, publisher, this.resolvableType);
350                }
351        }
352
353
354        /**
355         * Specialization of {@link HttpEntity} for use with a
356         * {@link Publisher}-based body, for which we also need to keep track of
357         * the element type.
358         * @param <T> the type contained in the publisher
359         * @param <P> the publisher
360         */
361        static final class PublisherEntity<T, P extends Publisher<T>> extends HttpEntity<P>
362                        implements ResolvableTypeProvider  {
363
364                private final ResolvableType resolvableType;
365
366                PublisherEntity(
367                                @Nullable MultiValueMap<String, String> headers, P publisher, ResolvableType resolvableType) {
368
369                        super(publisher, headers);
370                        Assert.notNull(publisher, "'publisher' must not be null");
371                        Assert.notNull(resolvableType, "'resolvableType' must not be null");
372                        this.resolvableType = resolvableType;
373                }
374
375                /**
376                 * Return the element type for the {@code Publisher} body.
377                 */
378                @Override
379                @NonNull
380                public ResolvableType getResolvableType() {
381                        return this.resolvableType;
382                }
383        }
384
385}