001/* 002 * Copyright 2002-2020 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.converter; 018 019import java.io.IOException; 020import java.io.OutputStream; 021import java.io.UnsupportedEncodingException; 022import java.net.URLDecoder; 023import java.net.URLEncoder; 024import java.nio.charset.Charset; 025import java.nio.charset.StandardCharsets; 026import java.util.ArrayList; 027import java.util.Collections; 028import java.util.LinkedHashMap; 029import java.util.List; 030import java.util.Map; 031 032import javax.mail.internet.MimeUtility; 033 034import org.springframework.core.io.Resource; 035import org.springframework.http.HttpEntity; 036import org.springframework.http.HttpHeaders; 037import org.springframework.http.HttpInputMessage; 038import org.springframework.http.HttpOutputMessage; 039import org.springframework.http.MediaType; 040import org.springframework.http.StreamingHttpOutputMessage; 041import org.springframework.lang.Nullable; 042import org.springframework.util.Assert; 043import org.springframework.util.CollectionUtils; 044import org.springframework.util.LinkedMultiValueMap; 045import org.springframework.util.MimeTypeUtils; 046import org.springframework.util.MultiValueMap; 047import org.springframework.util.StreamUtils; 048import org.springframework.util.StringUtils; 049 050/** 051 * Implementation of {@link HttpMessageConverter} to read and write 'normal' HTML 052 * forms and also to write (but not read) multipart data (e.g. file uploads). 053 * 054 * <p>In other words, this converter can read and write the 055 * {@code "application/x-www-form-urlencoded"} media type as 056 * {@link MultiValueMap MultiValueMap<String, String>}, and it can also 057 * write (but not read) the {@code "multipart/form-data"} and 058 * {@code "multipart/mixed"} media types as 059 * {@link MultiValueMap MultiValueMap<String, Object>}. 060 * 061 * <h3>Multipart Data</h3> 062 * 063 * <p>By default, {@code "multipart/form-data"} is used as the content type when 064 * {@linkplain #write writing} multipart data. As of Spring Framework 5.2 it is 065 * also possible to write multipart data using other multipart subtypes such as 066 * {@code "multipart/mixed"} and {@code "multipart/related"}, as long as the 067 * multipart subtype is registered as a {@linkplain #getSupportedMediaTypes 068 * supported media type} <em>and</em> the desired multipart subtype is specified 069 * as the content type when {@linkplain #write writing} the multipart data. Note 070 * that {@code "multipart/mixed"} is registered as a supported media type by 071 * default. 072 * 073 * <p>When writing multipart data, this converter uses other 074 * {@link HttpMessageConverter HttpMessageConverters} to write the respective 075 * MIME parts. By default, basic converters are registered for byte array, 076 * {@code String}, and {@code Resource}. These can be overridden via 077 * {@link #setPartConverters} or augmented via {@link #addPartConverter}. 078 * 079 * <h3>Examples</h3> 080 * 081 * <p>The following snippet shows how to submit an HTML form using the 082 * {@code "multipart/form-data"} content type. 083 * 084 * <pre class="code"> 085 * RestTemplate restTemplate = new RestTemplate(); 086 * // AllEncompassingFormHttpMessageConverter is configured by default 087 * 088 * MultiValueMap<String, Object> form = new LinkedMultiValueMap<>(); 089 * form.add("field 1", "value 1"); 090 * form.add("field 2", "value 2"); 091 * form.add("field 2", "value 3"); 092 * form.add("field 3", 4); // non-String form values supported as of 5.1.4 093 * 094 * restTemplate.postForLocation("https://example.com/myForm", form);</pre> 095 * 096 * <p>The following snippet shows how to do a file upload using the 097 * {@code "multipart/form-data"} content type. 098 * 099 * <pre class="code"> 100 * MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>(); 101 * parts.add("field 1", "value 1"); 102 * parts.add("file", new ClassPathResource("myFile.jpg")); 103 * 104 * restTemplate.postForLocation("https://example.com/myFileUpload", parts);</pre> 105 * 106 * <p>The following snippet shows how to do a file upload using the 107 * {@code "multipart/mixed"} content type. 108 * 109 * <pre class="code"> 110 * MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>(); 111 * parts.add("field 1", "value 1"); 112 * parts.add("file", new ClassPathResource("myFile.jpg")); 113 * 114 * HttpHeaders requestHeaders = new HttpHeaders(); 115 * requestHeaders.setContentType(MediaType.MULTIPART_MIXED); 116 * 117 * restTemplate.postForLocation("https://example.com/myFileUpload", 118 * new HttpEntity<>(parts, requestHeaders));</pre> 119 * 120 * <p>The following snippet shows how to do a file upload using the 121 * {@code "multipart/related"} content type. 122 * 123 * <pre class="code"> 124 * MediaType multipartRelated = new MediaType("multipart", "related"); 125 * 126 * restTemplate.getMessageConverters().stream() 127 * .filter(FormHttpMessageConverter.class::isInstance) 128 * .map(FormHttpMessageConverter.class::cast) 129 * .findFirst() 130 * .orElseThrow(() -> new IllegalStateException("Failed to find FormHttpMessageConverter")) 131 * .addSupportedMediaTypes(multipartRelated); 132 * 133 * MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>(); 134 * parts.add("field 1", "value 1"); 135 * parts.add("file", new ClassPathResource("myFile.jpg")); 136 * 137 * HttpHeaders requestHeaders = new HttpHeaders(); 138 * requestHeaders.setContentType(multipartRelated); 139 * 140 * restTemplate.postForLocation("https://example.com/myFileUpload", 141 * new HttpEntity<>(parts, requestHeaders));</pre> 142 * 143 * <h3>Miscellaneous</h3> 144 * 145 * <p>Some methods in this class were inspired by 146 * {@code org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}. 147 * 148 * @author Arjen Poutsma 149 * @author Rossen Stoyanchev 150 * @author Juergen Hoeller 151 * @author Sam Brannen 152 * @since 3.0 153 * @see org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter 154 * @see org.springframework.util.MultiValueMap 155 */ 156public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> { 157 158 /** 159 * The default charset used by the converter. 160 */ 161 public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; 162 163 private static final MediaType DEFAULT_FORM_DATA_MEDIA_TYPE = 164 new MediaType(MediaType.APPLICATION_FORM_URLENCODED, DEFAULT_CHARSET); 165 166 167 private List<MediaType> supportedMediaTypes = new ArrayList<>(); 168 169 private List<HttpMessageConverter<?>> partConverters = new ArrayList<>(); 170 171 private Charset charset = DEFAULT_CHARSET; 172 173 @Nullable 174 private Charset multipartCharset; 175 176 177 public FormHttpMessageConverter() { 178 this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED); 179 this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA); 180 this.supportedMediaTypes.add(MediaType.MULTIPART_MIXED); 181 182 this.partConverters.add(new ByteArrayHttpMessageConverter()); 183 this.partConverters.add(new StringHttpMessageConverter()); 184 this.partConverters.add(new ResourceHttpMessageConverter()); 185 186 applyDefaultCharset(); 187 } 188 189 190 /** 191 * Set the list of {@link MediaType} objects supported by this converter. 192 * @see #addSupportedMediaTypes(MediaType...) 193 * @see #getSupportedMediaTypes() 194 */ 195 public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) { 196 Assert.notNull(supportedMediaTypes, "'supportedMediaTypes' must not be null"); 197 // Ensure internal list is mutable. 198 this.supportedMediaTypes = new ArrayList<>(supportedMediaTypes); 199 } 200 201 /** 202 * Add {@link MediaType} objects to be supported by this converter. 203 * <p>The supplied {@code MediaType} objects will be appended to the list 204 * of {@linkplain #getSupportedMediaTypes() supported MediaType objects}. 205 * @param supportedMediaTypes a var-args list of {@code MediaType} objects to add 206 * @since 5.2 207 * @see #setSupportedMediaTypes(List) 208 */ 209 public void addSupportedMediaTypes(MediaType... supportedMediaTypes) { 210 Assert.notNull(supportedMediaTypes, "'supportedMediaTypes' must not be null"); 211 Assert.noNullElements(supportedMediaTypes, "'supportedMediaTypes' must not contain null elements"); 212 Collections.addAll(this.supportedMediaTypes, supportedMediaTypes); 213 } 214 215 /** 216 * {@inheritDoc} 217 * @see #setSupportedMediaTypes(List) 218 * @see #addSupportedMediaTypes(MediaType...) 219 */ 220 @Override 221 public List<MediaType> getSupportedMediaTypes() { 222 return Collections.unmodifiableList(this.supportedMediaTypes); 223 } 224 225 /** 226 * Set the message body converters to use. These converters are used to 227 * convert objects to MIME parts. 228 */ 229 public void setPartConverters(List<HttpMessageConverter<?>> partConverters) { 230 Assert.notEmpty(partConverters, "'partConverters' must not be empty"); 231 this.partConverters = partConverters; 232 } 233 234 /** 235 * Add a message body converter. Such a converter is used to convert objects 236 * to MIME parts. 237 */ 238 public void addPartConverter(HttpMessageConverter<?> partConverter) { 239 Assert.notNull(partConverter, "'partConverter' must not be null"); 240 this.partConverters.add(partConverter); 241 } 242 243 /** 244 * Set the default character set to use for reading and writing form data when 245 * the request or response {@code Content-Type} header does not explicitly 246 * specify it. 247 * <p>As of 4.3, this is also used as the default charset for the conversion 248 * of text bodies in a multipart request. 249 * <p>As of 5.0, this is also used for part headers including 250 * {@code Content-Disposition} (and its filename parameter) unless (the mutually 251 * exclusive) {@link #setMultipartCharset multipartCharset} is also set, in 252 * which case part headers are encoded as ASCII and <i>filename</i> is encoded 253 * with the {@code encoded-word} syntax from RFC 2047. 254 * <p>By default this is set to "UTF-8". 255 */ 256 public void setCharset(@Nullable Charset charset) { 257 if (charset != this.charset) { 258 this.charset = (charset != null ? charset : DEFAULT_CHARSET); 259 applyDefaultCharset(); 260 } 261 } 262 263 /** 264 * Apply the configured charset as a default to registered part converters. 265 */ 266 private void applyDefaultCharset() { 267 for (HttpMessageConverter<?> candidate : this.partConverters) { 268 if (candidate instanceof AbstractHttpMessageConverter) { 269 AbstractHttpMessageConverter<?> converter = (AbstractHttpMessageConverter<?>) candidate; 270 // Only override default charset if the converter operates with a charset to begin with... 271 if (converter.getDefaultCharset() != null) { 272 converter.setDefaultCharset(this.charset); 273 } 274 } 275 } 276 } 277 278 /** 279 * Set the character set to use when writing multipart data to encode file 280 * names. Encoding is based on the {@code encoded-word} syntax defined in 281 * RFC 2047 and relies on {@code MimeUtility} from {@code javax.mail}. 282 * <p>As of 5.0 by default part headers, including {@code Content-Disposition} 283 * (and its filename parameter) will be encoded based on the setting of 284 * {@link #setCharset(Charset)} or {@code UTF-8} by default. 285 * @since 4.1.1 286 * @see <a href="https://en.wikipedia.org/wiki/MIME#Encoded-Word">Encoded-Word</a> 287 */ 288 public void setMultipartCharset(Charset charset) { 289 this.multipartCharset = charset; 290 } 291 292 293 @Override 294 public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) { 295 if (!MultiValueMap.class.isAssignableFrom(clazz)) { 296 return false; 297 } 298 if (mediaType == null) { 299 return true; 300 } 301 for (MediaType supportedMediaType : getSupportedMediaTypes()) { 302 if (supportedMediaType.getType().equalsIgnoreCase("multipart")) { 303 // We can't read multipart, so skip this supported media type. 304 continue; 305 } 306 if (supportedMediaType.includes(mediaType)) { 307 return true; 308 } 309 } 310 return false; 311 } 312 313 @Override 314 public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) { 315 if (!MultiValueMap.class.isAssignableFrom(clazz)) { 316 return false; 317 } 318 if (mediaType == null || MediaType.ALL.equals(mediaType)) { 319 return true; 320 } 321 for (MediaType supportedMediaType : getSupportedMediaTypes()) { 322 if (supportedMediaType.isCompatibleWith(mediaType)) { 323 return true; 324 } 325 } 326 return false; 327 } 328 329 @Override 330 public MultiValueMap<String, String> read(@Nullable Class<? extends MultiValueMap<String, ?>> clazz, 331 HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { 332 333 MediaType contentType = inputMessage.getHeaders().getContentType(); 334 Charset charset = (contentType != null && contentType.getCharset() != null ? 335 contentType.getCharset() : this.charset); 336 String body = StreamUtils.copyToString(inputMessage.getBody(), charset); 337 338 String[] pairs = StringUtils.tokenizeToStringArray(body, "&"); 339 MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length); 340 for (String pair : pairs) { 341 int idx = pair.indexOf('='); 342 if (idx == -1) { 343 result.add(URLDecoder.decode(pair, charset.name()), null); 344 } 345 else { 346 String name = URLDecoder.decode(pair.substring(0, idx), charset.name()); 347 String value = URLDecoder.decode(pair.substring(idx + 1), charset.name()); 348 result.add(name, value); 349 } 350 } 351 return result; 352 } 353 354 @Override 355 @SuppressWarnings("unchecked") 356 public void write(MultiValueMap<String, ?> map, @Nullable MediaType contentType, HttpOutputMessage outputMessage) 357 throws IOException, HttpMessageNotWritableException { 358 359 if (isMultipart(map, contentType)) { 360 writeMultipart((MultiValueMap<String, Object>) map, contentType, outputMessage); 361 } 362 else { 363 writeForm((MultiValueMap<String, Object>) map, contentType, outputMessage); 364 } 365 } 366 367 368 private boolean isMultipart(MultiValueMap<String, ?> map, @Nullable MediaType contentType) { 369 if (contentType != null) { 370 return contentType.getType().equalsIgnoreCase("multipart"); 371 } 372 for (List<?> values : map.values()) { 373 for (Object value : values) { 374 if (value != null && !(value instanceof String)) { 375 return true; 376 } 377 } 378 } 379 return false; 380 } 381 382 private void writeForm(MultiValueMap<String, Object> formData, @Nullable MediaType contentType, 383 HttpOutputMessage outputMessage) throws IOException { 384 385 contentType = getFormContentType(contentType); 386 outputMessage.getHeaders().setContentType(contentType); 387 388 Charset charset = contentType.getCharset(); 389 Assert.notNull(charset, "No charset"); // should never occur 390 391 byte[] bytes = serializeForm(formData, charset).getBytes(charset); 392 outputMessage.getHeaders().setContentLength(bytes.length); 393 394 if (outputMessage instanceof StreamingHttpOutputMessage) { 395 StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage; 396 streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(bytes, outputStream)); 397 } 398 else { 399 StreamUtils.copy(bytes, outputMessage.getBody()); 400 } 401 } 402 403 /** 404 * Return the content type used to write forms, given the preferred content type. 405 * By default, this method returns the given content type, but adds the 406 * {@linkplain #setCharset(Charset) charset} if it does not have one. 407 * If {@code contentType} is {@code null}, 408 * {@code application/x-www-form-urlencoded; charset=UTF-8} is returned. 409 * <p>Subclasses can override this method to change this behavior. 410 * @param contentType the preferred content type (can be {@code null}) 411 * @return the content type to be used 412 * @since 5.2.2 413 */ 414 protected MediaType getFormContentType(@Nullable MediaType contentType) { 415 if (contentType == null) { 416 return DEFAULT_FORM_DATA_MEDIA_TYPE; 417 } 418 else if (contentType.getCharset() == null) { 419 return new MediaType(contentType, this.charset); 420 } 421 else { 422 return contentType; 423 } 424 } 425 426 protected String serializeForm(MultiValueMap<String, Object> formData, Charset charset) { 427 StringBuilder builder = new StringBuilder(); 428 formData.forEach((name, values) -> { 429 if (name == null) { 430 Assert.isTrue(CollectionUtils.isEmpty(values), "Null name in form data: " + formData); 431 return; 432 } 433 values.forEach(value -> { 434 try { 435 if (builder.length() != 0) { 436 builder.append('&'); 437 } 438 builder.append(URLEncoder.encode(name, charset.name())); 439 if (value != null) { 440 builder.append('='); 441 builder.append(URLEncoder.encode(String.valueOf(value), charset.name())); 442 } 443 } 444 catch (UnsupportedEncodingException ex) { 445 throw new IllegalStateException(ex); 446 } 447 }); 448 }); 449 450 return builder.toString(); 451 } 452 453 private void writeMultipart( 454 MultiValueMap<String, Object> parts, @Nullable MediaType contentType, HttpOutputMessage outputMessage) 455 throws IOException { 456 457 // If the supplied content type is null, fall back to multipart/form-data. 458 // Otherwise rely on the fact that isMultipart() already verified the 459 // supplied content type is multipart. 460 if (contentType == null) { 461 contentType = MediaType.MULTIPART_FORM_DATA; 462 } 463 464 byte[] boundary = generateMultipartBoundary(); 465 Map<String, String> parameters = new LinkedHashMap<>(2); 466 if (!isFilenameCharsetSet()) { 467 parameters.put("charset", this.charset.name()); 468 } 469 parameters.put("boundary", new String(boundary, StandardCharsets.US_ASCII)); 470 471 // Add parameters to output content type 472 contentType = new MediaType(contentType, parameters); 473 outputMessage.getHeaders().setContentType(contentType); 474 475 if (outputMessage instanceof StreamingHttpOutputMessage) { 476 StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage; 477 streamingOutputMessage.setBody(outputStream -> { 478 writeParts(outputStream, parts, boundary); 479 writeEnd(outputStream, boundary); 480 }); 481 } 482 else { 483 writeParts(outputMessage.getBody(), parts, boundary); 484 writeEnd(outputMessage.getBody(), boundary); 485 } 486 } 487 488 /** 489 * When {@link #setMultipartCharset(Charset)} is configured (i.e. RFC 2047, 490 * {@code encoded-word} syntax) we need to use ASCII for part headers, or 491 * otherwise we encode directly using the configured {@link #setCharset(Charset)}. 492 */ 493 private boolean isFilenameCharsetSet() { 494 return (this.multipartCharset != null); 495 } 496 497 private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException { 498 for (Map.Entry<String, List<Object>> entry : parts.entrySet()) { 499 String name = entry.getKey(); 500 for (Object part : entry.getValue()) { 501 if (part != null) { 502 writeBoundary(os, boundary); 503 writePart(name, getHttpEntity(part), os); 504 writeNewLine(os); 505 } 506 } 507 } 508 } 509 510 @SuppressWarnings("unchecked") 511 private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException { 512 Object partBody = partEntity.getBody(); 513 if (partBody == null) { 514 throw new IllegalStateException("Empty body for part '" + name + "': " + partEntity); 515 } 516 Class<?> partType = partBody.getClass(); 517 HttpHeaders partHeaders = partEntity.getHeaders(); 518 MediaType partContentType = partHeaders.getContentType(); 519 for (HttpMessageConverter<?> messageConverter : this.partConverters) { 520 if (messageConverter.canWrite(partType, partContentType)) { 521 Charset charset = isFilenameCharsetSet() ? StandardCharsets.US_ASCII : this.charset; 522 HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os, charset); 523 multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody)); 524 if (!partHeaders.isEmpty()) { 525 multipartMessage.getHeaders().putAll(partHeaders); 526 } 527 ((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage); 528 return; 529 } 530 } 531 throw new HttpMessageNotWritableException("Could not write request: no suitable HttpMessageConverter " + 532 "found for request type [" + partType.getName() + "]"); 533 } 534 535 /** 536 * Generate a multipart boundary. 537 * <p>This implementation delegates to 538 * {@link MimeTypeUtils#generateMultipartBoundary()}. 539 */ 540 protected byte[] generateMultipartBoundary() { 541 return MimeTypeUtils.generateMultipartBoundary(); 542 } 543 544 /** 545 * Return an {@link HttpEntity} for the given part Object. 546 * @param part the part to return an {@link HttpEntity} for 547 * @return the part Object itself it is an {@link HttpEntity}, 548 * or a newly built {@link HttpEntity} wrapper for that part 549 */ 550 protected HttpEntity<?> getHttpEntity(Object part) { 551 return (part instanceof HttpEntity ? (HttpEntity<?>) part : new HttpEntity<>(part)); 552 } 553 554 /** 555 * Return the filename of the given multipart part. This value will be used for the 556 * {@code Content-Disposition} header. 557 * <p>The default implementation returns {@link Resource#getFilename()} if the part is a 558 * {@code Resource}, and {@code null} in other cases. Can be overridden in subclasses. 559 * @param part the part to determine the file name for 560 * @return the filename, or {@code null} if not known 561 */ 562 @Nullable 563 protected String getFilename(Object part) { 564 if (part instanceof Resource) { 565 Resource resource = (Resource) part; 566 String filename = resource.getFilename(); 567 if (filename != null && this.multipartCharset != null) { 568 filename = MimeDelegate.encode(filename, this.multipartCharset.name()); 569 } 570 return filename; 571 } 572 else { 573 return null; 574 } 575 } 576 577 578 private void writeBoundary(OutputStream os, byte[] boundary) throws IOException { 579 os.write('-'); 580 os.write('-'); 581 os.write(boundary); 582 writeNewLine(os); 583 } 584 585 private static void writeEnd(OutputStream os, byte[] boundary) throws IOException { 586 os.write('-'); 587 os.write('-'); 588 os.write(boundary); 589 os.write('-'); 590 os.write('-'); 591 writeNewLine(os); 592 } 593 594 private static void writeNewLine(OutputStream os) throws IOException { 595 os.write('\r'); 596 os.write('\n'); 597 } 598 599 600 /** 601 * Implementation of {@link org.springframework.http.HttpOutputMessage} used 602 * to write a MIME multipart. 603 */ 604 private static class MultipartHttpOutputMessage implements HttpOutputMessage { 605 606 private final OutputStream outputStream; 607 608 private final Charset charset; 609 610 private final HttpHeaders headers = new HttpHeaders(); 611 612 private boolean headersWritten = false; 613 614 public MultipartHttpOutputMessage(OutputStream outputStream, Charset charset) { 615 this.outputStream = outputStream; 616 this.charset = charset; 617 } 618 619 @Override 620 public HttpHeaders getHeaders() { 621 return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); 622 } 623 624 @Override 625 public OutputStream getBody() throws IOException { 626 writeHeaders(); 627 return this.outputStream; 628 } 629 630 private void writeHeaders() throws IOException { 631 if (!this.headersWritten) { 632 for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) { 633 byte[] headerName = getBytes(entry.getKey()); 634 for (String headerValueString : entry.getValue()) { 635 byte[] headerValue = getBytes(headerValueString); 636 this.outputStream.write(headerName); 637 this.outputStream.write(':'); 638 this.outputStream.write(' '); 639 this.outputStream.write(headerValue); 640 writeNewLine(this.outputStream); 641 } 642 } 643 writeNewLine(this.outputStream); 644 this.headersWritten = true; 645 } 646 } 647 648 private byte[] getBytes(String name) { 649 return name.getBytes(this.charset); 650 } 651 } 652 653 654 /** 655 * Inner class to avoid a hard dependency on the JavaMail API. 656 */ 657 private static class MimeDelegate { 658 659 public static String encode(String value, String charset) { 660 try { 661 return MimeUtility.encodeText(value, charset, null); 662 } 663 catch (UnsupportedEncodingException ex) { 664 throw new IllegalStateException(ex); 665 } 666 } 667 } 668 669}