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<Account> accountMono = ... 073 * builder.asyncPart("account", accountMono).header("foo", "bar"); 074 * 075 * // Build and use 076 * MultiValueMap<String, HttpEntity<?>> multipartBody = builder.build(); 077 * 078 * Mono<Void> 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}